Skip to content

Commit 1765a78

Browse files
committed
feat(scenarios): screenshots read a built view container through the spec's binding, and the first shot's page is visited once unshot
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
1 parent 15da5ac commit 1765a78

2 files changed

Lines changed: 35 additions & 10 deletions

File tree

‎packages/qa/scenarios/src/export/screenshots.ts‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { existsSync, mkdirSync } from 'node:fs';
3434
import { dirname, join } from 'node:path';
3535

3636
import { resolveStackCollection } from '@objectstack/core';
37-
import { expandViewContainer } from '@objectstack/spec/ui';
37+
import { deriveViewContainerObject, expandViewContainer } from '@objectstack/spec/ui';
3838

3939
import type { LoadedScenario } from '../loader.js';
4040
import { scenarioNamespace } from '../schema.js';
@@ -81,22 +81,23 @@ export function taggedViewKinds(card: Pick<LoadedScenario['card'], 'capabilities
8181

8282
const nameOf = (v: unknown): string | undefined => (typeof v === 'object' && v !== null && typeof (v as { name?: unknown }).name === 'string' ? (v as { name: string }).name : undefined);
8383

84-
/** Every list view the build declares: `{ object, name: '<object>.<key>', type }`, in declaration order. */
84+
/** Every list view the build declares: `{ object, name: '<object>.<key>', type }`, in declaration order, containers expanded by the spec's own composer. */
8585
export function declaredListViews(artifact: unknown): Array<{ object: string; name: string; type: string }> {
8686
const out: Array<{ object: string; name: string; type: string }> = [];
8787
for (const raw of resolveStackCollection(artifact, 'views')) {
8888
if (typeof raw !== 'object' || raw === null) continue;
8989
const item = raw as Record<string, unknown>;
90-
const object = typeof item.object === 'string' ? item.object : nameOf(item);
91-
if (!object) continue;
9290
if ('list' in item || 'listViews' in item) {
91+
// The platform's one spelling of a container's binding (a built container usually carries no `object` of its own).
92+
const object = deriveViewContainerObject(item);
93+
if (!object) continue;
9394
for (const v of expandViewContainer(object, item)) {
9495
if (v.viewKind !== 'list') continue;
9596
const type = (v.config as { type?: unknown } | undefined)?.type;
9697
out.push({ object, name: v.name, type: typeof type === 'string' ? type : 'grid' });
9798
}
98-
} else if (typeof item.type === 'string' && typeof item.name === 'string') {
99-
out.push({ object, name: item.name.includes('.') ? item.name : `${object}.${item.name}`, type: item.type });
99+
} else if (typeof item.type === 'string' && typeof item.name === 'string' && typeof item.object === 'string') {
100+
out.push({ object: item.object, name: item.name.includes('.') ? item.name : `${item.object}.${item.name}`, type: item.type });
100101
}
101102
}
102103
return out;
@@ -240,12 +241,24 @@ export function playwrightBrowser(opts: PlaywrightBrowserOptions = {}): Screensh
240241
const signIn = await context.request.post(`${baseUrl}/api/v1/auth/sign-in/email`, { data: admin });
241242
if (!signIn.ok()) throw new Error(`the browser's admin sign-in was refused: ${signIn.status()}`);
242243
const page = await context.newPage();
244+
const visit = async (path: string) => {
245+
await page.goto(`${baseUrl}${path}`, { waitUntil: 'load' });
246+
// A page that never goes quiet (a stream, a poll) is shot after the budget, not skipped.
247+
await page.waitForLoadState('networkidle', { timeout: opts.settleTimeoutMs ?? 30_000 }).catch(() => undefined);
248+
await page.waitForTimeout(1_000);
249+
};
250+
// An administrator's first visit to an app page can open first-run UI over it (measured on
251+
// 18.3: a default-currency dialog over the first app page, once per browser session; the
252+
// Console's landing page does not take it). So the first shot's page is visited once
253+
// unshot, and nothing is shot under that dialog.
254+
let warmed = false;
243255
return {
244256
async capture(path, file) {
245-
await page.goto(`${baseUrl}${path}`, { waitUntil: 'load' });
246-
// A page that never goes quiet (a stream, a poll) is shot after the budget, not skipped.
247-
await page.waitForLoadState('networkidle', { timeout: opts.settleTimeoutMs ?? 30_000 }).catch(() => undefined);
248-
await page.waitForTimeout(1_000);
257+
if (!warmed) {
258+
await visit(path);
259+
warmed = true;
260+
}
261+
await visit(path);
249262
await page.screenshot({ path: file });
250263
},
251264
close: () => browser.close(),

‎packages/qa/scenarios/test/export/screenshots.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ describe('planShots', () => {
9494
expect(plan.skipped.map((s) => s.kind)).toEqual(['list', 'record', 'dashboard']);
9595
});
9696

97+
it('reads a container as `os build` writes it: no `object` of its own, the binding in `list.data.object`', () => {
98+
// The shape measured from a create-objectstack 18.3 project's dist/objectstack.json.
99+
const built = {
100+
list: { label: 'All accounts', type: 'grid', data: { provider: 'object', object: 'demoorder_account' }, columns: [{ field: 'name' }] },
101+
listViews: { board: { label: 'By industry', type: 'kanban', data: { provider: 'object', object: 'demoorder_account' }, columns: ['name'], kanban: { groupByField: 'industry' } } },
102+
};
103+
expect(declaredListViews(orderBuild({ views: [built] }))).toEqual([
104+
{ object: 'demoorder_account', name: 'demoorder_account.board', type: 'kanban' },
105+
{ object: 'demoorder_account', name: 'demoorder_account.default', type: 'grid' },
106+
]);
107+
});
108+
97109
it('reads a container through the spec composer: the default list is `<object>.default`, a standalone view keeps its name', () => {
98110
expect(declaredListViews(orderBuild({ views: [{ object: 'demoorder_invoice', list: { type: 'grid' } }, { name: 'demoorder_account.board', object: 'demoorder_account', type: 'kanban' }] }))).toEqual([
99111
{ object: 'demoorder_invoice', name: 'demoorder_invoice.default', type: 'grid' },

0 commit comments

Comments
 (0)