Skip to content

Commit 962206c

Browse files
committed
test(ui): pin the round-3 locale reads under zh and their English controls (#25916)
Claim-free WIP on the card branch. Claude-Session: https://claude.ai/code/session_01C6T942afhqAP5w9XBmfTDC Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8a62e09 commit 962206c

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #25916, third pass — Flow Runs' client-capability diagnostics speak the
5+
* session's language.
6+
*
7+
* Each of the page's three panels calls one client method (`meta.getItems`,
8+
* `automation.execute`, `automation.listRuns`). A client without it used to
9+
* leave an English sentence in the panel's error state. The state now keeps
10+
* the method's NAME and the panel words it at render through
11+
* `console.developer.flowRuns.clientMethodUnavailable`, so under zh the
12+
* sentence is the zh row with the method name in it. A thrown message is
13+
* still drawn verbatim. Control: with no provider the English sentence is
14+
* unchanged.
15+
*/
16+
17+
import { afterEach, describe, expect, it, vi } from 'vitest';
18+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
19+
import { MemoryRouter } from 'react-router-dom';
20+
import { I18nProvider } from '@object-ui/i18n';
21+
import { builtInLocales } from '@object-ui/i18n/locales';
22+
23+
const CLIENT = vi.hoisted(() => ({ current: {} as Record<string, unknown> }));
24+
const ADAPTER = vi.hoisted(() => ({ getClient: () => CLIENT.current }));
25+
vi.mock('@object-ui/app-shell', async (importOriginal) => ({
26+
...(await importOriginal<Record<string, unknown>>()),
27+
useAdapter: () => ADAPTER,
28+
useMetadata: () => ({ objects: [], getTypeStatus: () => 'loaded' }),
29+
}));
30+
31+
// Imported after the mock so the page picks it up.
32+
import { FlowRunsPage } from './FlowRunsPage';
33+
34+
afterEach(() => cleanup());
35+
36+
const { zh } = builtInLocales;
37+
const zhFlow = zh.console.developer.flowRuns;
38+
39+
/** A client that serves one flow but has no automation surface at all. */
40+
const NO_AUTOMATION = { meta: { getItems: async () => ({ type: 'flow', items: [{ name: 'welcome_flow' }] }) } };
41+
/** A client that throws from the one method it has. */
42+
const THROWS = { meta: { getItems: async () => { throw new Error('boom: 503'); } } };
43+
44+
function page(language: 'zh' | null) {
45+
const routed = (
46+
<MemoryRouter>
47+
<FlowRunsPage />
48+
</MemoryRouter>
49+
);
50+
return render(
51+
language ? (
52+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
53+
{routed}
54+
</I18nProvider>
55+
) : (
56+
routed
57+
),
58+
);
59+
}
60+
61+
const unavailable = (method: string) => zhFlow.clientMethodUnavailable.replace('{{method}}', method);
62+
63+
describe('Flow Runs client diagnostics under zh (#25916)', () => {
64+
it('guard: the zh row is a real zh row', () => {
65+
expect(zhFlow.clientMethodUnavailable).toMatch(/\p{Script=Han}/u);
66+
expect(zhFlow.clientMethodUnavailable).toContain('{{method}}');
67+
});
68+
69+
it('a client without meta.getItems: the flow panel says so in zh', async () => {
70+
CLIENT.current = {};
71+
page('zh');
72+
expect(await screen.findByText(unavailable('meta.getItems'))).toBeTruthy();
73+
});
74+
75+
it('a client without automation: the runs panel and a test run say so in zh', async () => {
76+
CLIENT.current = NO_AUTOMATION;
77+
page('zh');
78+
expect(await screen.findByText(unavailable('automation.listRuns'))).toBeTruthy();
79+
fireEvent.click(screen.getByRole('button', { name: zhFlow.runFlow }));
80+
expect(await screen.findByText(unavailable('automation.execute'))).toBeTruthy();
81+
});
82+
83+
it('a thrown message is still drawn verbatim', async () => {
84+
CLIENT.current = THROWS;
85+
page('zh');
86+
expect(await screen.findByText('boom: 503')).toBeTruthy();
87+
});
88+
});
89+
90+
describe('control: with no provider the diagnostics read their English (#25916)', () => {
91+
it('meta.getItems', async () => {
92+
CLIENT.current = {};
93+
page(null);
94+
expect(await screen.findByText('meta.getItems is not available on this client')).toBeTruthy();
95+
});
96+
97+
it('automation.listRuns and automation.execute', async () => {
98+
CLIENT.current = NO_AUTOMATION;
99+
page(null);
100+
expect(await screen.findByText('automation.listRuns is not available on this client')).toBeTruthy();
101+
fireEvent.click(screen.getByRole('button', { name: 'Run Flow' }));
102+
expect(await screen.findByText('automation.execute is not available on this client')).toBeTruthy();
103+
});
104+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* #25916, third pass — the References sheet's table headers read
5+
* `engine.edit.refsCol*` from this module's own `engine.*` table (the Studio
6+
* carve-out, `packages/i18n/README.md`), so a zh session reads its column
7+
* names in zh where they were English literals. Control: an English session
8+
* still reads From type / From name / Path.
9+
*/
10+
11+
import '@testing-library/jest-dom/vitest';
12+
import { describe, it, expect, vi, afterEach } from 'vitest';
13+
import { fireEvent, render, screen, cleanup, waitFor, within } from '@testing-library/react';
14+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
15+
import { I18nProvider } from '@object-ui/i18n';
16+
import { t } from './i18n';
17+
18+
const objectDef = {
19+
name: 'showcase_account',
20+
label: 'Account',
21+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
22+
};
23+
24+
/** A plain packaged entry — this suite is about the References sheet only. */
25+
const LAYERED = {
26+
code: { ...objectDef, _packageId: 'com.example.showcase', _provenance: 'package' },
27+
overlay: null,
28+
effective: { ...objectDef },
29+
provenance: 'package',
30+
packageId: 'com.example.showcase',
31+
editable: true,
32+
deletable: true,
33+
resettable: true,
34+
};
35+
36+
const referencesImpl = {
37+
current: vi.fn(async () => [{ fromType: 'view', fromName: 'all_accounts', path: 'data.object' }]),
38+
};
39+
40+
const mockClient = {
41+
list: vi.fn(async () => []),
42+
listDrafts: vi.fn(async () => []),
43+
get: vi.fn(async () => null),
44+
getDraft: vi.fn(async () => null),
45+
layered: vi.fn(async () => LAYERED),
46+
reset: vi.fn(async () => ({ success: true })),
47+
references: vi.fn(async (...args: unknown[]) => referencesImpl.current(...args)),
48+
};
49+
50+
vi.mock('./useMetadata', async (importOriginal) => {
51+
const mod = await importOriginal<typeof import('./useMetadata')>();
52+
return {
53+
...mod,
54+
useMetadataClient: () => mockClient,
55+
useMetadataTypes: () => ({
56+
loading: false,
57+
error: null,
58+
entries: [
59+
{
60+
type: 'object',
61+
name: 'object',
62+
label: 'Object',
63+
allowEnvironmentOverlay: true,
64+
allowRuntimeCreate: true,
65+
schema: {
66+
type: 'object',
67+
properties: { name: { type: 'string' }, label: { type: 'string' } },
68+
},
69+
},
70+
],
71+
}),
72+
};
73+
});
74+
75+
import { MetadataResourceEditPage } from './ResourceEditPage';
76+
77+
const START_PATH = '/metadata/object/showcase_account';
78+
79+
function editor(language: 'zh' | 'en') {
80+
return render(
81+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
82+
<MemoryRouter initialEntries={[START_PATH]}>
83+
<Routes>
84+
<Route
85+
path="/metadata/:type/:name"
86+
element={<MetadataResourceEditPage type="object" name="showcase_account" />}
87+
/>
88+
</Routes>
89+
</MemoryRouter>
90+
</I18nProvider>,
91+
);
92+
}
93+
94+
/** Open the References sheet (the lazy load) and return its table's header cells. */
95+
async function headersIn(locale: 'zh-CN' | 'en-US') {
96+
await waitFor(() => expect(mockClient.layered).toHaveBeenCalled());
97+
fireEvent.click(await screen.findByTitle(t('engine.edit.references', locale)));
98+
const name = await screen.findByText('all_accounts');
99+
const table = name.closest('table') as HTMLElement;
100+
return within(table).getAllByRole('columnheader').map((th) => th.textContent);
101+
}
102+
103+
afterEach(() => {
104+
cleanup();
105+
vi.clearAllMocks();
106+
});
107+
108+
describe('References sheet column headers (#25916)', () => {
109+
it('guard: the zh rows are real zh, not English copies', () => {
110+
for (const key of ['engine.edit.refsColFromType', 'engine.edit.refsColFromName', 'engine.edit.refsColPath']) {
111+
expect(t(key, 'zh-CN')).not.toBe(t(key, 'en-US'));
112+
expect(t(key, 'zh-CN')).toMatch(/\p{Script=Han}/u);
113+
}
114+
});
115+
116+
it('a zh session reads the column names in zh', async () => {
117+
editor('zh');
118+
expect(await headersIn('zh-CN')).toEqual([
119+
t('engine.edit.refsColFromType', 'zh-CN'),
120+
t('engine.edit.refsColFromName', 'zh-CN'),
121+
t('engine.edit.refsColPath', 'zh-CN'),
122+
]);
123+
});
124+
125+
it('control: an English session still reads From type / From name / Path', async () => {
126+
editor('en');
127+
expect(await headersIn('en-US')).toEqual(['From type', 'From name', 'Path']);
128+
});
129+
});

0 commit comments

Comments
 (0)