@mars-stack/cli 8.0.1 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.cursor/rules/mars-testing.mdc +1 -1
- package/template/e2e/README.md +51 -34
- package/template/e2e/blog-disabled.spec.ts +12 -0
- package/template/e2e-kitchen-sink/kitchen-sink-catalog.spec.ts +179 -0
- package/template/e2e-kitchen-sink/playwright.config.ts +33 -0
package/package.json
CHANGED
|
@@ -27,4 +27,4 @@ alwaysApply: false
|
|
|
27
27
|
|
|
28
28
|
End-to-end specs live in **`e2e/`** (`yarn test:e2e`). When you add or materially change **user-facing** behaviour (pages, flows, CLI-generated features), extend **`e2e/*.spec.ts`** so at least one **stable happy path** is covered. Prefer role/label selectors; avoid sleep-only assertions.
|
|
29
29
|
|
|
30
|
-
**Mars monorepo:**
|
|
30
|
+
**Mars monorepo:** Catalog and CI expectations are in **`docs/design-docs/scaffold-testing-strategy.md`** (Decision 7). Update **`template/e2e/`** (baseline), **`template/e2e-kitchen-sink/`** (kitchen-sink catalog), and **`template/e2e/README.md`** when you change generators or template routes. See **MARS-041** (done).
|
package/template/e2e/README.md
CHANGED
|
@@ -10,7 +10,54 @@ Browser-level tests for user-visible behaviour. Run: `yarn test:e2e` from the pr
|
|
|
10
10
|
|
|
11
11
|
Monorepo CI runs this on pull requests via the `template-e2e` job in `.github/workflows/ci.yml` (Postgres service + `E2E_USE_CI_SERVER=1` + `scripts/start-e2e-server.mjs`).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
## Kitchen-sink CLI catalog (MARS-041, Option A)
|
|
14
|
+
|
|
15
|
+
Full **`mars generate`** surface is exercised against a **materialized kitchen-sink fixture** (not the bare template tree):
|
|
16
|
+
|
|
17
|
+
| Mechanism | Location |
|
|
18
|
+
|-----------|----------|
|
|
19
|
+
| Spec + Playwright config (source) | `template/e2e-kitchen-sink/` |
|
|
20
|
+
| Runner (materialize → copy spec → build → Playwright) | `scripts/run-kitchen-sink-e2e.ts` |
|
|
21
|
+
| Shared materialization (also used by Tier 2 matrix) | `scripts/scaffold-fixture-materialize.ts` |
|
|
22
|
+
| Local / CI | `yarn test:e2e:kitchen-sink` — CI job `kitchen-sink-e2e` on PRs when `template/`, `packages/cli/`, or runner scripts change |
|
|
23
|
+
|
|
24
|
+
## `mars generate` subcommands — inventory
|
|
25
|
+
|
|
26
|
+
Canonical list: `packages/cli/src/index.ts` (`generate` subcommands). **Status** points to where behaviour is covered.
|
|
27
|
+
|
|
28
|
+
| CLI command | Spec file(s) | Status |
|
|
29
|
+
|-------------|--------------|--------|
|
|
30
|
+
| _(baseline)_ | `auth.spec.ts`, `public.spec.ts`, `api.spec.ts`, `smoke.spec.ts` | Partial — template auth, public, API, CSRF smoke |
|
|
31
|
+
| `blog` | `blog-disabled.spec.ts` (template: feature off → 404) | **Kitchen-sink:** `e2e-kitchen-sink/kitchen-sink-catalog.spec.ts` |
|
|
32
|
+
| `dark-mode` | — | **Kitchen-sink** catalog |
|
|
33
|
+
| `notifications` | — | **Kitchen-sink** catalog |
|
|
34
|
+
| `analytics` | — | **Kitchen-sink** catalog (ConsentBanner) |
|
|
35
|
+
| `command-palette` | — | **Kitchen-sink** catalog |
|
|
36
|
+
| `onboarding` | — | **Kitchen-sink** catalog |
|
|
37
|
+
| `search` | — | **Kitchen-sink** catalog |
|
|
38
|
+
| `realtime` | — | **Kitchen-sink** catalog (SSE open) |
|
|
39
|
+
| `ai` | — | **Kitchen-sink** catalog (UI + no-key / disabled send) |
|
|
40
|
+
| `cookie-consent` | — | **Kitchen-sink** catalog |
|
|
41
|
+
| `coming-soon` | — | **Kitchen-sink** catalog |
|
|
42
|
+
| `sentry` | — | **Kitchen-sink** catalog (no client `pageerror`) |
|
|
43
|
+
| `feature-flags` | — | **Kitchen-sink** catalog (protected GET JSON) |
|
|
44
|
+
|
|
45
|
+
## Scaffold-time features (`mars create`, user-visible)
|
|
46
|
+
|
|
47
|
+
| Area | Spec file(s) | Status |
|
|
48
|
+
|------|--------------|--------|
|
|
49
|
+
| Billing | — | **Kitchen-sink** catalog (`/pricing`) |
|
|
50
|
+
| File upload | — | **Kitchen-sink** catalog (`/files`) |
|
|
51
|
+
|
|
52
|
+
## Waivers
|
|
53
|
+
|
|
54
|
+
| Item | Reason |
|
|
55
|
+
|------|--------|
|
|
56
|
+
| **`mars add` subcommands** | Out of scope for MARS-041 — no required new specs per ticket non-goals |
|
|
57
|
+
| **Generators with no stable UI** | None in the MARS-041 table; if added, document here with rationale |
|
|
58
|
+
| **Mobile project in CI** | `playwright.config.ts` includes **mobile** for local runs; **`template-e2e`** and **`kitchen-sink-e2e`** run **desktop Chromium only** in CI to control minutes — intentional |
|
|
59
|
+
|
|
60
|
+
## CI recipe for generated apps
|
|
14
61
|
|
|
15
62
|
Copy the **`template-e2e`** job from the Mars repo’s `.github/workflows/ci.yml` into your project workflow, or mirror its steps:
|
|
16
63
|
|
|
@@ -22,44 +69,14 @@ Copy the **`template-e2e`** job from the Mars repo’s `.github/workflows/ci.yml
|
|
|
22
69
|
|
|
23
70
|
If `DATABASE_URL` is missing or wrong, API routes fail at runtime and mutations can surface as 500s instead of deterministic CSRF 403s — fix env before interpreting smoke failures.
|
|
24
71
|
|
|
25
|
-
## Monorepo constraint (MARS-041)
|
|
26
|
-
|
|
27
|
-
The Mars **template package** in this repo is the pre-generator baseline. Most `mars generate` features are **not** present under `template/src/features/` until generators run (as in `scripts/test-scaffold-matrix.ts` with the **kitchen-sink** fixture). Full CLI catalog E2E in CI therefore requires **Option A** from MARS-041: scaffold a max-feature app to a working directory, provision `JWT_SECRET` / `DATABASE_URL`, then run Playwright from **that** tree. Specs in this folder exercise whatever the committed template actually contains (today: auth, public, API baseline); the README inventory below tracks **target** coverage once the scaffolded-app E2E job exists.
|
|
28
|
-
|
|
29
72
|
## When to update
|
|
30
73
|
|
|
31
74
|
Whenever you add or change a feature that ships **UI or routes** (including features originally added via the Mars CLI’s `generate` commands), add or extend a spec here and update the tables below.
|
|
32
75
|
|
|
33
|
-
**Mars monorepo contributors:** Follow **Decision 7** in `docs/design-docs/scaffold-testing-strategy.md
|
|
76
|
+
**Mars monorepo contributors:** Follow **Decision 7** in `docs/design-docs/scaffold-testing-strategy.md`. For CLI-only surfaces, update **`template/e2e-kitchen-sink/kitchen-sink-catalog.spec.ts`** and keep this inventory in sync.
|
|
34
77
|
|
|
35
78
|
Use **local / console / `none`** providers so tests do not require paid third-party accounts.
|
|
36
79
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
| CLI command | Spec file(s) | Status |
|
|
40
|
-
|-------------|--------------|--------|
|
|
41
|
-
| _(baseline)_ | `auth.spec.ts`, `public.spec.ts`, `api.spec.ts` | Partial (auth, public, API smoke) |
|
|
42
|
-
| `blog` | — | Missing |
|
|
43
|
-
| `dark-mode` | — | Missing |
|
|
44
|
-
| `notifications` | — | Missing |
|
|
45
|
-
| `analytics` | — | Missing |
|
|
46
|
-
| `command-palette` | — | Missing |
|
|
47
|
-
| `onboarding` | — | Missing |
|
|
48
|
-
| `search` | — | Missing |
|
|
49
|
-
| `realtime` | — | Missing |
|
|
50
|
-
| `ai` | — | Missing |
|
|
51
|
-
| `cookie-consent` | — | Missing |
|
|
52
|
-
| `coming-soon` | — | Missing |
|
|
53
|
-
| `sentry` | — | Missing |
|
|
54
|
-
| `feature-flags` | — | Missing |
|
|
55
|
-
|
|
56
|
-
## Scaffold-time features (`mars create`, user-visible)
|
|
57
|
-
|
|
58
|
-
| Area | Spec file(s) | Status |
|
|
59
|
-
|------|--------------|--------|
|
|
60
|
-
| Billing | — | Missing |
|
|
61
|
-
| File upload | — | Missing |
|
|
62
|
-
|
|
63
|
-
## Waivers
|
|
80
|
+
## Non-goals (MARS-041)
|
|
64
81
|
|
|
65
|
-
|
|
82
|
+
- `mars add page|model|component|email` — no required new specs in MARS-041; defer to future tickets.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Baseline template ships with blog feature off — /blog must not exist (no crash).
|
|
5
|
+
* Kitchen-sink catalog (MARS-041) covers blog when generators run.
|
|
6
|
+
*/
|
|
7
|
+
test.describe('Feature disabled: blog (template baseline)', () => {
|
|
8
|
+
test('blog route shows not-found when feature not generated', async ({ page }) => {
|
|
9
|
+
await page.goto('/blog');
|
|
10
|
+
await expect(page.getByRole('heading', { name: 'Page not found' })).toBeVisible();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import type { Page } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
const SEEDED_USER_EMAIL = 'user@example.com';
|
|
5
|
+
const SEEDED_USER_PASSWORD = 'Password123!';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Kitchen-sink enables onboarding + dashboard redirect until flow completes.
|
|
9
|
+
*/
|
|
10
|
+
async function signInWithSeededUser(page: Page): Promise<void> {
|
|
11
|
+
await page.goto('/sign-in');
|
|
12
|
+
await page.getByLabel('Email Address').fill(SEEDED_USER_EMAIL);
|
|
13
|
+
await page.getByLabel('Password', { exact: true }).fill(SEEDED_USER_PASSWORD);
|
|
14
|
+
await page.getByRole('button', { name: /^sign in$/i }).click();
|
|
15
|
+
await page.waitForURL(/\/dashboard|\/onboarding/, { timeout: 60_000 });
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < 10 && page.url().includes('/onboarding'); i++) {
|
|
18
|
+
const complete = page.getByRole('button', { name: 'Complete Step' });
|
|
19
|
+
await expect(complete).toBeVisible({ timeout: 30_000 });
|
|
20
|
+
await complete.click();
|
|
21
|
+
await page.waitForURL(/\/dashboard|\/onboarding/, { timeout: 60_000 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test.describe('Kitchen-sink CLI catalog (MARS-041)', () => {
|
|
28
|
+
test.describe.configure({ timeout: 120_000 });
|
|
29
|
+
|
|
30
|
+
test('blog: listing page shows Blog heading', async ({ page }) => {
|
|
31
|
+
await page.goto('/blog');
|
|
32
|
+
await expect(page.getByRole('heading', { name: 'Blog' })).toBeVisible();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('dark-mode: theme toggle is present and clickable', async ({ page }) => {
|
|
36
|
+
await page.goto('/');
|
|
37
|
+
const toggle = page.getByRole('button', { name: /switch to .* theme/i });
|
|
38
|
+
await expect(toggle.first()).toBeVisible();
|
|
39
|
+
await toggle.first().click();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('notifications: bell control is visible', async ({ page }) => {
|
|
43
|
+
await page.goto('/dashboard');
|
|
44
|
+
await expect(page.getByRole('button', { name: /^Notifications/i })).toBeVisible();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('analytics: consent banner accept dismisses banner', async ({ page }) => {
|
|
48
|
+
await page.goto('/');
|
|
49
|
+
await expect(page.getByText('Cookie usage')).toBeVisible();
|
|
50
|
+
await page.getByRole('button', { name: 'Accept' }).click();
|
|
51
|
+
await expect(page.getByText('Cookie usage')).not.toBeVisible();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('command-palette: Ctrl+K opens palette and navigation action works', async ({ page }) => {
|
|
55
|
+
await page.goto('/dashboard');
|
|
56
|
+
await page.keyboard.press('Control+K');
|
|
57
|
+
await expect(page.getByPlaceholder('Type a command or search...')).toBeVisible();
|
|
58
|
+
await page.getByPlaceholder('Type a command or search...').fill('settings');
|
|
59
|
+
await page.getByRole('option', { name: /go to settings/i }).click();
|
|
60
|
+
await expect(page).toHaveURL(/\/settings/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('onboarding: new user completes steps and reaches dashboard', async ({ page }) => {
|
|
64
|
+
await page.goto('/sign-in');
|
|
65
|
+
await page.getByLabel('Email Address').fill(SEEDED_USER_EMAIL);
|
|
66
|
+
await page.getByLabel('Password', { exact: true }).fill(SEEDED_USER_PASSWORD);
|
|
67
|
+
await page.getByRole('button', { name: /^sign in$/i }).click();
|
|
68
|
+
await page.waitForURL(/\/dashboard|\/onboarding/, { timeout: 60_000 });
|
|
69
|
+
if (page.url().includes('/onboarding')) {
|
|
70
|
+
await expect(
|
|
71
|
+
page.getByRole('heading', { name: /welcome! let's get you set up/i }),
|
|
72
|
+
).toBeVisible();
|
|
73
|
+
}
|
|
74
|
+
await signInWithSeededUser(page);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('search: nav search input accepts text (debounce surface)', async ({ page }) => {
|
|
78
|
+
await page.goto('/dashboard');
|
|
79
|
+
const search = page.getByPlaceholder('Search…');
|
|
80
|
+
await expect(search).toBeVisible();
|
|
81
|
+
await search.fill('test query');
|
|
82
|
+
await expect(search).toHaveValue('test query');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('realtime: authenticated EventSource opens SSE stream', async ({ page }) => {
|
|
86
|
+
await signInWithSeededUser(page);
|
|
87
|
+
const result = await page.evaluate(() => {
|
|
88
|
+
return new Promise<'open' | 'error'>((resolve) => {
|
|
89
|
+
const es = new EventSource('/api/protected/realtime/stream?channel=e2e-smoke');
|
|
90
|
+
const timer = window.setTimeout(() => {
|
|
91
|
+
es.close();
|
|
92
|
+
resolve('error');
|
|
93
|
+
}, 20_000);
|
|
94
|
+
es.onopen = () => {
|
|
95
|
+
window.clearTimeout(timer);
|
|
96
|
+
es.close();
|
|
97
|
+
resolve('open');
|
|
98
|
+
};
|
|
99
|
+
es.onerror = () => {
|
|
100
|
+
window.clearTimeout(timer);
|
|
101
|
+
es.close();
|
|
102
|
+
resolve('error');
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
expect(result).toBe('open');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('ai: chat page shows UI; send disabled or missing-key error (no live LLM)', async ({
|
|
110
|
+
page,
|
|
111
|
+
}) => {
|
|
112
|
+
await signInWithSeededUser(page);
|
|
113
|
+
await page.goto('/ai');
|
|
114
|
+
await expect(page.getByRole('heading', { name: 'AI Assistant' })).toBeVisible();
|
|
115
|
+
await expect(page.getByText(/send a message to begin chatting/i)).toBeVisible();
|
|
116
|
+
const send = page.getByRole('button', { name: 'Send message' });
|
|
117
|
+
const errorBanner = page.getByText(/OPENAI_API_KEY|API key|not set/i);
|
|
118
|
+
await expect(send.or(errorBanner).first()).toBeVisible();
|
|
119
|
+
if (await send.isVisible()) {
|
|
120
|
+
await expect(send).toBeDisabled();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('cookie-consent: decline dismisses banner', async ({ page, context }) => {
|
|
125
|
+
await context.clearCookies();
|
|
126
|
+
await page.goto('/');
|
|
127
|
+
await expect(page.getByText('Cookie usage')).toBeVisible();
|
|
128
|
+
await page.getByRole('button', { name: 'Decline' }).click();
|
|
129
|
+
await expect(page.getByText('Cookie usage')).not.toBeVisible();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('coming-soon: waitlist form submits and shows thanks', async ({ page }) => {
|
|
133
|
+
await page.goto('/coming-soon');
|
|
134
|
+
await expect(page.getByRole('heading', { name: 'Coming Soon' })).toBeVisible();
|
|
135
|
+
await page.getByPlaceholder('Enter your email').fill('waitlist-e2e@example.com');
|
|
136
|
+
await page.getByRole('button', { name: /join waitlist/i }).click();
|
|
137
|
+
await expect(page.getByText(/thanks for joining/i)).toBeVisible();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('sentry: home page loads without client error overlay', async ({ page }) => {
|
|
141
|
+
const errors: string[] = [];
|
|
142
|
+
page.on('pageerror', (e) => errors.push(e.message));
|
|
143
|
+
await page.goto('/');
|
|
144
|
+
await expect(page.locator('h1').first()).toBeVisible();
|
|
145
|
+
expect(errors, errors.join('; ')).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('feature-flags: authenticated flag API returns JSON', async ({ page }) => {
|
|
149
|
+
await signInWithSeededUser(page);
|
|
150
|
+
const data = await page.evaluate(async () => {
|
|
151
|
+
const res = await fetch('/api/protected/feature-flags?key=nonexistent-flag-e2e', {
|
|
152
|
+
credentials: 'include',
|
|
153
|
+
});
|
|
154
|
+
const json: unknown = await res.json();
|
|
155
|
+
return { status: res.status, json };
|
|
156
|
+
});
|
|
157
|
+
expect(data.status).toBe(200);
|
|
158
|
+
expect(data.json).toMatchObject({ value: false });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('billing: pricing page loads when billing enabled', async ({ page }) => {
|
|
162
|
+
await page.goto('/pricing');
|
|
163
|
+
await expect(page.getByRole('heading', { name: 'Simple, transparent pricing' })).toBeVisible();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('file upload: files page loads', async ({ page }) => {
|
|
167
|
+
await signInWithSeededUser(page);
|
|
168
|
+
await page.goto('/files');
|
|
169
|
+
await expect(page.getByRole('heading', { name: 'Files' })).toBeVisible();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('auth: unauthenticated user cannot access dashboard', async ({ browser }) => {
|
|
173
|
+
const context = await browser.newContext();
|
|
174
|
+
const page = await context.newPage();
|
|
175
|
+
await page.goto('/dashboard');
|
|
176
|
+
await expect(page).toHaveURL(/sign-in/);
|
|
177
|
+
await context.close();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Playwright config for MARS-041 kitchen-sink scaffold E2E.
|
|
5
|
+
* Invoked from repo root via `scripts/run-kitchen-sink-e2e.ts` with cwd = materialized project dir.
|
|
6
|
+
*/
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
testDir: '.',
|
|
9
|
+
testMatch: 'kitchen-sink-catalog.spec.ts',
|
|
10
|
+
fullyParallel: false,
|
|
11
|
+
forbidOnly: !!process.env.CI,
|
|
12
|
+
retries: process.env.CI ? 2 : 0,
|
|
13
|
+
workers: 1,
|
|
14
|
+
reporter: process.env.CI ? 'line' : 'html',
|
|
15
|
+
use: {
|
|
16
|
+
baseURL: 'http://localhost:3000',
|
|
17
|
+
trace: 'on-first-retry',
|
|
18
|
+
screenshot: 'only-on-failure',
|
|
19
|
+
},
|
|
20
|
+
projects: [
|
|
21
|
+
{
|
|
22
|
+
name: 'chromium',
|
|
23
|
+
use: { ...devices['Desktop Chrome'] },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
webServer: {
|
|
27
|
+
command: 'node scripts/start-e2e-server.mjs',
|
|
28
|
+
url: 'http://localhost:3000',
|
|
29
|
+
reuseExistingServer: false,
|
|
30
|
+
timeout: 240_000,
|
|
31
|
+
env: { ...process.env, MARS_CI_KITCHEN_SINK_E2E: '1' },
|
|
32
|
+
},
|
|
33
|
+
});
|