Skip to content

Commit 7aeb7e5

Browse files
test(e2e): add admin smoke test covering the tinacms upgrade regressions
Three Playwright assertions against the TinaCMS admin at /admin/index.html. Each one catches a specific failure mode we hit during the 3.4 -> 3.8 upgrade so a future regression is caught up front: 1. "loads without the 'Failed loading assets' placeholder" -- catches the IPv4/IPv6 binding mismatch. If tina-ipv4-proxy.js dies or the Dockerfile entrypoint loses its spawn line, the admin/index.html's hardcoded http://localhost:4001/... script tags can't resolve and Tina renders its built-in error placeholder. The assertion sees that. 2. "every collection in tina/config.ts is reachable by URL" -- walks each #/collections/<name> hash route and asserts its page header renders. Catches schema regressions (collection removed, renamed, broken). Uses location.hash mutation rather than page.goto so Tina's React app doesn't full-reload and re-render the edit-mode modal mid-test. 3. "clicking a row title opens the form-only admin editor, not visual edit" -- asserts that the title click on a News row routes to /collections/edit/news/... rather than the broken visual-edit iframe. Re-adding `ui.router` to any collection would break this. Headless playwright contexts have no Tina session cookie so they see the one-time "Enter Edit Mode" modal that real users only see once. The shared dismissEditModeModal helper waits for it (networkidle fires before the modal renders) and clicks it.
1 parent 4f2c9c8 commit 7aeb7e5

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

e2e/admin.spec.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { test, expect, Page } from '@playwright/test';
2+
3+
// Smoke test for the TinaCMS admin shell at /admin/index.html.
4+
//
5+
// Catches the two regressions we hit during the 3.4 -> 3.8 upgrade:
6+
//
7+
// 1. @tinacms/cli@2.1.8+ init hang -- if `tinacms dev` never reaches
8+
// its "Dev Server is active" state, `ng serve` is never spawned and
9+
// the page never loads at all (connection refused on :4200).
10+
//
11+
// 2. `tinacms dev` binds only to ::1:4001 -- the admin's hardcoded
12+
// `http://localhost:4001/...` script tags can't resolve via the
13+
// IPv4-only docker port-publish. Tina then renders its built-in
14+
// "Failed loading TinaCMS assets" placeholder. The IPv4 forwarder
15+
// at projects/website-angular/src/scripts/tina-ipv4-proxy.js must
16+
// be running for the assets to load.
17+
//
18+
// Note on the "Enter Edit Mode" modal: a fresh playwright context has
19+
// no Tina session cookie, so Tina shows a one-time modal before exposing
20+
// the sidebar/collections. A real human only sees this once.
21+
async function dismissEditModeModal(page: Page) {
22+
// Wait for the modal to actually render (networkidle fires before Tina's
23+
// React app mounts it). Short timeout because if no modal appears, we
24+
// were already past it.
25+
const enterEdit = page.getByRole('button', { name: /enter edit mode/i });
26+
try {
27+
await enterEdit.waitFor({ state: 'visible', timeout: 5000 });
28+
await enterEdit.click();
29+
await page.waitForTimeout(1500);
30+
} catch {
31+
// No modal -- already in edit mode.
32+
}
33+
}
34+
35+
async function enterAdmin(page: Page) {
36+
await page.goto('/admin/index.html', { waitUntil: 'networkidle' });
37+
await dismissEditModeModal(page);
38+
}
39+
40+
test.describe('TinaCMS admin shell', () => {
41+
test('loads without the "Failed loading assets" placeholder', async ({ page }) => {
42+
await page.goto('/admin/index.html', { waitUntil: 'networkidle' });
43+
await expect(page).toHaveTitle(/TinaCMS/i);
44+
await expect(page.locator('#no-assets-placeholder')).toHaveCount(0);
45+
await expect(page.locator('text=Failed loading TinaCMS assets')).toHaveCount(0);
46+
});
47+
48+
test('every collection in tina/config.ts is reachable by URL', async ({ page }) => {
49+
// Driving Tina's collapsible sidebar via the hamburger is fragile (the
50+
// hamburger is layered behind the llama logo SVG which intercepts
51+
// pointer events). Hitting each collection's hash URL exercises the
52+
// same code path -- if Tina rejects the URL the page header label
53+
// doesn't render. This catches schema regressions (collection
54+
// removed, renamed, or broken by a config change).
55+
await page.setViewportSize({ width: 1440, height: 900 });
56+
await enterAdmin(page);
57+
58+
for (const { name, label } of [
59+
{ name: 'about', label: 'About' },
60+
{ name: 'news', label: 'News' },
61+
{ name: 'content', label: 'Content' },
62+
{ name: 'reactome_research_spotlights', label: 'Reactome Research Spotlights' },
63+
{ name: 'documentation', label: 'Documentation' },
64+
{ name: 'community', label: 'Community' },
65+
]) {
66+
await page.evaluate((n) => {
67+
window.location.hash = `#/collections/${n}`;
68+
}, name);
69+
await page.waitForTimeout(1500);
70+
await expect(page.getByRole('heading', { name: label, exact: true })).toBeVisible({
71+
timeout: 10000,
72+
});
73+
}
74+
});
75+
76+
test('clicking a row title opens the form-only admin editor, not visual edit', async ({
77+
page,
78+
}) => {
79+
// Tina's collection table needs horizontal room. The default 1280x720
80+
// viewport renders the table in a layout where rows don't materialize
81+
// until enough columns fit.
82+
await page.setViewportSize({ width: 1440, height: 900 });
83+
await enterAdmin(page);
84+
85+
// News is the worst case for visual editing: long filenames hide the
86+
// kebab "Edit in Admin" action off-screen. With `ui.router` removed
87+
// (commit 4f2c9c8 prior), the title click instead routes to the
88+
// form-only admin editor. Re-adding ui.router would re-introduce the
89+
// visual-edit iframe trap and this assertion would fail.
90+
// Don't `page.goto` to a new URL -- it triggers a full reload that
91+
// re-renders the edit-mode modal even after we dismissed it in
92+
// enterAdmin. Mutating location.hash keeps Tina's React app mounted
93+
// and just routes within it.
94+
await page.evaluate(() => {
95+
window.location.hash = '#/collections/news';
96+
});
97+
await page.waitForTimeout(2500);
98+
const firstRowLink = page.locator('tbody tr a').first();
99+
await expect(firstRowLink).toBeVisible({ timeout: 15000 });
100+
await firstRowLink.click();
101+
102+
await expect(page).toHaveURL(/\/admin\/index\.html#\/collections\/edit\/news\//, {
103+
timeout: 10000,
104+
});
105+
});
106+
});

0 commit comments

Comments
 (0)