Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ module.exports = {
"@typescript-eslint/no-require-imports": "off",
},
},
{
// Jest manual mocks are CommonJS modules (module.exports / require) that
// run in the Node test environment — give them the right globals so the
// `no-undef` / require rules don't flag legitimate mock plumbing.
files: ["__mocks__/**"],
env: { node: true, jest: true },
rules: {
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-var-requires": "off",
},
},
],
ignorePatterns: ["node_modules/", ".expo/", "dist/", "*.config.js", "babel.config.js", "server/hotcrm/"],
};
3 changes: 3 additions & 0 deletions .github/workflows/eas-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
build:
name: EAS Build (${{ inputs.platform || 'all' }})
runs-on: ubuntu-latest
# Skip (neutral, not failed) when no Expo token is configured — EAS needs an
# account credential this repo can't provide by default.
if: ${{ secrets.EXPO_TOKEN != '' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/eas-submit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
submit:
name: Submit to ${{ inputs.platform == 'ios' && 'App Store' || 'Google Play' }}
runs-on: ubuntu-latest
# Skip (neutral, not failed) when no Expo token is configured.
if: ${{ secrets.EXPO_TOKEN != '' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/eas-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
update:
name: Publish OTA Update
runs-on: ubuntu-latest
# Skip (neutral, not failed) when no Expo token is configured.
if: ${{ secrets.EXPO_TOKEN != '' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand Down
25 changes: 23 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,69 @@ jobs:
steps:
- uses: actions/checkout@v4

# The integration stack (server/integration) is optional and not always
# present in the repo. Detect it up front and skip the server steps when
# absent so this job is a clean no-op instead of a hard failure on the
# missing working directory.
- name: Check for integration stack
id: stack
run: |
if [ -d server/integration ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::server/integration not present — skipping server integration tests."
fi

- uses: pnpm/action-setup@v4
if: steps.stack.outputs.present == 'true'
with:
version: 10

- uses: actions/setup-node@v4
if: steps.stack.outputs.present == 'true'
with:
node-version: 20
cache: pnpm

# Install app + CLI dependencies
- name: Install dependencies
if: steps.stack.outputs.present == 'true'
run: pnpm install

# Pre-compile the stack to a JSON artifact (surfaces any compile error in
# its own step and lets the server boot without an esbuild compile).
- name: Build integration artifact
if: steps.stack.outputs.present == 'true'
run: npx objectstack build
working-directory: server/integration

# Start the ObjectStack CLI server (server/integration stack) in background
- name: Start integration server
if: steps.stack.outputs.present == 'true'
run: ./scripts/start-integration-server.sh --bg
env:
PORT: 4000
AUTH_SECRET: integration-secret-please-change-min-32-chars

# Wait for server readiness
- name: Wait for server
if: steps.stack.outputs.present == 'true'
run: ./scripts/wait-for-server.sh http://localhost:4000/api/v1/auth/get-session 120

# Run integration tests
- name: Run integration tests
if: steps.stack.outputs.present == 'true'
run: pnpm test:integration:server
env:
INTEGRATION_SERVER_URL: http://localhost:4000

# Surface the server log for diagnosis if anything above failed
- name: Dump server log
if: always()
if: always() && steps.stack.outputs.present == 'true'
run: cat .integration-server.log || true

# Stop server
- name: Stop integration server
if: always()
if: always() && steps.stack.outputs.present == 'true'
run: ./scripts/stop-integration-server.sh
39 changes: 23 additions & 16 deletions __tests__/e2e/app-navigation.e2e.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,26 @@ import SearchScreen from "~/app/(tabs)/search";
import AppsScreen from "~/app/(tabs)/apps";
import NotificationsScreen from "~/app/(tabs)/notifications";
import MoreScreen from "~/app/(tabs)/more";
import { ToastProvider } from "~/components/ui/Toast";
import { ConfirmProvider } from "~/components/ui/ConfirmDialog";

describe("E2E: App Navigation — Tab Screens", () => {
it("renders Home tab header and empty dashboards state", async () => {
const { getByText, findByText } = render(<HomeScreen />);

expect(getByText("Dashboard")).toBeTruthy();
expect(getByText("Welcome back. Here's your overview.")).toBeTruthy();
// Home leads with a time-of-day greeting (asserted via the stable subtitle)
// and surfaces the AI assistant entry.
expect(getByText("Here's your overview.")).toBeTruthy();
expect(getByText("AI Assistant")).toBeTruthy();
// Apps publish no dashboards in this mock, so Home shows its empty state.
expect(await findByText("No Dashboards")).toBeTruthy();
});

it("renders Search tab with search input", () => {
const { getByPlaceholderText, getByText } = render(<SearchScreen />);

expect(
getByPlaceholderText("Search objects, records..."),
).toBeTruthy();
expect(
getByText("Search across all your objects and records"),
).toBeTruthy();
expect(getByPlaceholderText("Search across all records…")).toBeTruthy();
expect(getByText("Search across all your records")).toBeTruthy();
expect(getByText("Type to start searching")).toBeTruthy();
});

Expand All @@ -118,19 +118,26 @@ describe("E2E: App Navigation — Tab Screens", () => {
});

it("renders More tab with menu sections", () => {
const { getByText } = render(<MoreScreen />);
// MoreScreen consumes the Toast + Confirm contexts (sign-out flow).
const { getByText } = render(
<ToastProvider>
<ConfirmProvider>
<MoreScreen />
</ConfirmProvider>
</ToastProvider>,
);

expect(getByText("Test User")).toBeTruthy();
expect(getByText("test@example.com")).toBeTruthy();
// Section headers.
expect(getByText("Account")).toBeTruthy();
expect(getByText("Assistant")).toBeTruthy();
expect(getByText("Automation")).toBeTruthy();
expect(getByText("Preferences")).toBeTruthy();
expect(getByText("Appearance")).toBeTruthy();
// Menu items.
expect(getByText("Account & Security")).toBeTruthy();
expect(getByText("AI Assistant")).toBeTruthy();
expect(getByText("Language")).toBeTruthy();
expect(getByText("Security")).toBeTruthy();
expect(getByText("Security & Privacy")).toBeTruthy();
expect(getByText("Settings")).toBeTruthy();
expect(getByText("Support")).toBeTruthy();
expect(getByText("Help & Support")).toBeTruthy();
expect(getByText("About")).toBeTruthy();
expect(getByText("Sign Out")).toBeTruthy();
});
});
19 changes: 4 additions & 15 deletions __tests__/e2e/auth-flow.e2e.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import { Alert } from "react-native";

/* ---- Mocks ---- */

Expand Down Expand Up @@ -40,8 +39,6 @@ jest.mock("~/stores/server-store", () => ({
selector({ ssoProviders: ["google"] }),
}));

jest.spyOn(Alert, "alert");

import SignInScreen from "~/app/(auth)/sign-in";

describe("E2E: Authentication Flow", () => {
Expand All @@ -67,10 +64,8 @@ describe("E2E: Authentication Flow", () => {

fireEvent.press(getByText("Sign In"));

expect(Alert.alert).toHaveBeenCalledWith(
"Error",
"Please fill in all fields.",
);
// The screen surfaces an inline error rather than a native Alert.
expect(getByText("Please enter your email and password.")).toBeTruthy();
expect(mockSignInEmail).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -119,10 +114,7 @@ describe("E2E: Authentication Flow", () => {
fireEvent.press(getByText("Sign In"));

await waitFor(() => {
expect(Alert.alert).toHaveBeenCalledWith(
"Sign In Failed",
"Invalid credentials",
);
expect(getByText("Invalid credentials")).toBeTruthy();
});
expect(mockReplace).not.toHaveBeenCalled();
});
Expand All @@ -143,10 +135,7 @@ describe("E2E: Authentication Flow", () => {
fireEvent.press(getByText("Sign In"));

await waitFor(() => {
expect(Alert.alert).toHaveBeenCalledWith(
"Error",
"Something went wrong. Please try again.",
);
expect(getByText("Something went wrong. Please try again.")).toBeTruthy();
});
});

Expand Down
10 changes: 10 additions & 0 deletions __tests__/e2e/e2e.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* E2E test setup.
*
* Initialize i18n so the rendered screens show their real (English) copy rather
* than raw translation keys — the e2e suite renders full screens that call
* `useTranslation`, and without an initialized instance `t(key)` returns the
* key. Importing the app's i18n module runs its synchronous inline-resource
* init (no async backend), so translations resolve immediately.
*/
import "~/lib/i18n";
6 changes: 3 additions & 3 deletions __tests__/e2e/record-list.e2e.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ let mockAppDiscoveryReturn: {
refetch: jest.Mock;
};

jest.mock("~/hooks/useAppDiscovery", () => ({
useAppDiscovery: () => mockAppDiscoveryReturn,
jest.mock("~/hooks/useApps", () => ({
useApps: () => mockAppDiscoveryReturn,
}));

import AppsScreen from "~/app/(tabs)/apps";
Expand Down Expand Up @@ -93,7 +93,7 @@ describe("E2E: Record List — App Discovery & Navigation", () => {

const { getByText } = render(<AppsScreen />);

expect(getByText("Loading apps…")).toBeTruthy();
expect(getByText("Loading your apps…")).toBeTruthy();
});

it("shows error state with retry button", () => {
Expand Down
40 changes: 40 additions & 0 deletions __tests__/lib/formatting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
formatNumber,
formatPercent,
formatCurrency,
formatByPattern,
} from "~/lib/formatting";

describe("formatDate", () => {
Expand Down Expand Up @@ -66,3 +67,42 @@ describe("formatCurrency", () => {
expect(result).toMatch(/€|EUR/);
});
});

describe("formatDateTime — invalid input", () => {
it("returns the raw value for an unparseable date", () => {
expect(formatDateTime("nope")).toBe("nope");
});
});

describe("formatByPattern", () => {
it("falls back to a grouped number when no pattern is given", () => {
expect(formatByPattern(1234)).toBe("1,234");
});

it("returns an em dash for non-finite values", () => {
expect(formatByPattern(Infinity, "$0,0")).toBe("—");
expect(formatByPattern(NaN)).toBe("—");
});

it("formats a percent pattern with the declared fraction digits", () => {
expect(formatByPattern(50, "0%")).toBe("50%");
expect(formatByPattern(12.5, "0.0%")).toBe("12.5%");
});

it("formats a currency pattern", () => {
expect(formatByPattern(1000, "$0,0")).toContain("$");
expect(formatByPattern(1000, "$0,0")).toContain("1,000");
expect(formatByPattern(9.99, "$0,0.00")).toContain("9.99");
});

it("abbreviates with a compact notation pattern", () => {
expect(formatByPattern(1500, "0a")).toMatch(/1\.5\s?K/i);
const m = formatByPattern(1_500_000, "$0.0a");
expect(m).toContain("$");
expect(m).toMatch(/1\.5\s?M/i);
});

it("honours fraction digits in a plain decimal pattern", () => {
expect(formatByPattern(1234.5, "0,0.0")).toBe("1,234.5");
});
});
Loading
Loading