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
42 changes: 42 additions & 0 deletions .github/workflows/showcase-smoke.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
# Showcase Smoke — NON-BLOCKING. Drives the console (served by the backend at
# /_console) across every showcase nav surface, asserting render health
# (no crash / no leaked dev placeholder / charts draw). Manual + nightly only;
# it never gates PRs. Promote to a PR gate once it has proven stable here.
name: Showcase Smoke

on:
workflow_dispatch:
schedule:
- cron: '0 7 * * *' # 07:00 UTC nightly

permissions:
contents: read

jobs:
smoke:
name: Showcase nav-surface smoke
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# The smoke's webServer runs the CLI (`os serve --dev`); build it first.
- run: pnpm turbo run build --filter=@objectstack/cli
- name: Install Playwright Chromium
working-directory: examples/app-showcase
run: pnpm exec playwright install --with-deps chromium
- name: Run showcase smoke
working-directory: examples/app-showcase
run: pnpm test:smoke
- name: Upload report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: showcase-smoke-report
path: examples/app-showcase/playwright-report/
retention-days: 7
4 changes: 4 additions & 0 deletions examples/app-showcase/.gitignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@

# Playwright smoke artifacts
test-results/
playwright-report/
1 change: 1 addition & 0 deletions examples/app-showcase/e2e/.gitignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
.auth/
33 changes: 33 additions & 0 deletions examples/app-showcase/e2e/global-setup.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
import { request } from '@playwright/test';
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';

/**
* Auth for the showcase smoke: sign in against the backend (better-auth) and
* persist a Playwright storageState. The console (served at :3000/_console)
* reads a Bearer token from localStorage `auth-session-token` on the :3000
* origin, so we inject that there; the session cookie covers the API.
*/
const API = process.env.SMOKE_API_URL || 'http://localhost:3000';
const EMAIL = process.env.SMOKE_EMAIL || 'admin@objectos.ai';
const PASSWORD = process.env.SMOKE_PASSWORD || 'admin123';
const STATE_PATH = 'e2e/.auth/state.json';

export default async function globalSetup() {
const ctx = await request.newContext();
const res = await ctx.post(`${API}/api/v1/auth/sign-in/email`, {
data: { email: EMAIL, password: PASSWORD },
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok()) throw new Error(`sign-in failed (${res.status()}): ${await res.text()}`);
const token = res.headers()['set-auth-token'];
if (!token) throw new Error('no set-auth-token header from sign-in');
const apiState = await ctx.storageState();
await ctx.dispose();
const state = {
cookies: apiState.cookies,
origins: [{ origin: API, localStorage: [{ name: 'auth-session-token', value: token }] }],
};
mkdirSync(dirname(STATE_PATH), { recursive: true });
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}
70 changes: 70 additions & 0 deletions examples/app-showcase/e2e/showcase-smoke.spec.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
import { test, expect } from '@playwright/test';

/**
* Showcase workspace smoke — sweeps every nav surface and asserts the health
* invariants manual QA otherwise eyeballs (render crash / leaked dev placeholder
* / collapsed chart). Runs against the console the backend serves at /_console
* (baseURL set in playwright.config.ts). Non-blocking nightly + manual.
*/
const APP = process.env.SHOWCASE_APP || 'com.example.showcase';
const base = (seg: string) => `/_console/apps/${APP}/${seg}`;

const SURFACES: { name: string; path: string; chart?: boolean }[] = [
{ name: 'My Work', path: base('page/showcase_my_work') },
{ name: 'Approvals', path: base('page/showcase_review_queue') },
{ name: 'New Project Wizard', path: base('page/showcase_new_project_wizard') },
{ name: 'Settings', path: base('showcase_preference') },
{ name: 'Projects', path: base('showcase_project') },
{ name: 'Tasks', path: base('showcase_task') },
{ name: 'Accounts', path: base('showcase_account') },
{ name: 'Invoices', path: base('showcase_invoice') },
{ name: 'Products', path: base('showcase_product') },
{ name: 'Teams', path: base('showcase_team') },
{ name: 'Categories', path: base('showcase_category') },
{ name: 'Field Zoo', path: base('showcase_field_zoo') },
{ name: 'Delivery Operations', path: base('dashboard/showcase_ops_dashboard'), chart: true },
{ name: 'Chart Gallery', path: base('dashboard/showcase_chart_gallery'), chart: true },
{ name: 'Hours by Status', path: base('report/showcase_hours_by_status') },
{ name: 'Status × Priority', path: base('report/showcase_status_priority_matrix') },
{ name: 'Task Overview', path: base('report/showcase_task_overview') },
{ name: 'Component Gallery', path: base('page/showcase_component_gallery') },
{ name: 'Project Workspace', path: base('page/showcase_project_workspace') },
{ name: 'Task Workbench', path: base('page/showcase_task_workbench') },
{ name: 'Task Triage', path: base('page/showcase_task_triage') },
{ name: 'Active Projects', path: base('page/showcase_active_projects') },
{ name: 'All Views', path: base('page/showcase_task_all_views') },
{ name: 'Task Board', path: base('page/showcase_task_board') },
{ name: 'Task Calendar', path: base('page/showcase_task_calendar') },
{ name: 'Task Gallery', path: base('page/showcase_task_gallery') },
{ name: 'Team Schedule', path: base('page/showcase_task_schedule') },
{ name: 'Activity Timeline', path: base('page/showcase_task_timeline') },
{ name: 'Work Map', path: base('page/showcase_task_map') },
];

for (const surface of SURFACES) {
test(`surface renders cleanly: ${surface.name}`, async ({ page }) => {
const pageErrors: string[] = [];
page.on('pageerror', (e) => pageErrors.push(e.message));

await page.goto(surface.path, { waitUntil: 'domcontentloaded' });
await page.locator('main').first().waitFor({ state: 'visible', timeout: 25_000 });
await page.waitForTimeout(1500);

expect(pageErrors, `uncaught errors on ${surface.name}`).toEqual([]);
await expect(
page.getByText(/no actions configured/i),
`leaked placeholder on ${surface.name}`,
).toHaveCount(0);
const mainText = (await page.locator('main').first().innerText().catch(() => '')) || '';
expect(mainText.trim().length, `${surface.name} rendered no main content`).toBeGreaterThan(0);

if (surface.chart) {
const svg = page.locator('.recharts-wrapper svg, .recharts-surface').first();
await svg.waitFor({ state: 'visible', timeout: 25_000 });
const box = await svg.boundingBox();
expect(box, `${surface.name}: no chart SVG`).not.toBeNull();
expect(box!.width, `${surface.name}: chart width`).toBeGreaterThan(0);
expect(box!.height, `${surface.name}: chart height`).toBeGreaterThan(0);
}
});
}
4 changes: 3 additions & 1 deletion examples/app-showcase/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,8 @@
"build": "objectstack build",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"verify": "pnpm typecheck && pnpm test"
"verify": "pnpm typecheck && pnpm test",
"test:smoke": "playwright test --config=playwright.config.ts"
},
"dependencies": {
"@objectstack/cloud-connection": "workspace:*",
Expand All@@ -29,6 +30,7 @@
"devDependencies": {
"@objectstack/cli": "workspace:*",
"@objectstack/objectql": "workspace:*",
"@playwright/test": "^1.61.0",
"typescript": "^6.0.3",
"vitest": "^4.1.9"
}
Expand Down
35 changes: 35 additions & 0 deletions examples/app-showcase/playwright.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test';

/**
* Showcase smoke — drives the console (served by the backend at /_console)
* across every nav surface. `webServer` boots the real backend so CI only needs
* to run `playwright test`; locally it reuses an already-running :3000.
*
* Run: pnpm --filter @objectstack/example-showcase exec playwright test
* (or via the ci/showcase-smoke workflow). Non-blocking by design.
*/
const PORT = 3000;
export default defineConfig({
testDir: './e2e',
fullyParallel: false,
workers: 1,
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI ? [['github'], ['list']] : [['list']],
globalSetup: './e2e/global-setup.ts',
timeout: 45_000,
use: {
baseURL: `http://localhost:${PORT}`,
storageState: 'e2e/.auth/state.json',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: 'node node_modules/@objectstack/cli/bin/run.js serve --dev',
url: `http://localhost:${PORT}/api/v1/runtime/config`,
timeout: 180_000,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
},
});
12 changes: 12 additions & 0 deletions examples/app-showcase/vitest.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import { defineConfig, configDefaults } from 'vitest/config';

/**
* Unit tests only. The Playwright browser smoke under `e2e/` also uses
* `*.spec.ts`, so exclude it here — otherwise vitest tries to run it and chokes
* on the `@playwright/test` import. Run the smoke with `pnpm test:smoke`.
*/
export default defineConfig({
test: {
exclude: [...configDefaults.exclude, '**/e2e/**'],
},
});
Loading