Skip to content

Commit 1727215

Browse files
committed
fix: correct EBSR author import and add strategy-aware smoke matrix
Rewrite synced configure/lib imports to /author so EBSR author loads correctly, and reuse the smoke matrix for ESM-by-default runs with optional esm,iife parity coverage. Made-with: Cursor
1 parent c928659 commit 1727215

6 files changed

Lines changed: 203 additions & 51 deletions

File tree

apps/element-demo/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
"predev": "bun run generate-imports && bun run --cwd ../.. verify:no-local-paths apps/element-demo/src/lib/element-imports.js apps/element-demo/src/lib/element-imports.d.ts",
1414
"test:e2e": "playwright test",
1515
"test:e2e:iife": "RUN_IIFE_E2E=1 playwright test iife-usefulness.spec.ts",
16-
"test:e2e:iife:suite": "RUN_IIFE_E2E=1 playwright test iife-usefulness.spec.ts smoke-matrix.spec.ts",
17-
"test:e2e:iife:suite:orchestrated": "PIE_IIFE_EXTERNAL_SERVER=1 RUN_IIFE_E2E=1 playwright test iife-usefulness.spec.ts smoke-matrix.spec.ts",
16+
"test:e2e:iife:suite": "RUN_IIFE_E2E=1 MATRIX_STRATEGIES=esm,iife playwright test iife-usefulness.spec.ts smoke-matrix.spec.ts",
17+
"test:e2e:iife:suite:orchestrated": "PIE_IIFE_EXTERNAL_SERVER=1 RUN_IIFE_E2E=1 MATRIX_STRATEGIES=esm,iife playwright test iife-usefulness.spec.ts smoke-matrix.spec.ts",
1818
"test:iife:bundle": "bun run scripts/test-iife-bundle-contract.ts",
1919
"test:e2e:ui": "playwright test --ui",
2020
"test:e2e:headed": "playwright test --headed",

apps/element-demo/test/e2e/smoke-matrix.spec.ts

Lines changed: 190 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import { test, type Page } from '@playwright/test';
22
import { ELEMENT_REGISTRY } from '../../src/lib/elements/registry';
3+
import {
4+
deliveryContainer,
5+
getSessionState,
6+
interactOnce,
7+
switchMode,
8+
switchRole,
9+
waitForSessionMutation,
10+
} from './test-helpers';
311

412
type ViewKind = 'deliver' | 'author' | 'print';
13+
type StrategyKind = 'esm' | 'iife';
514

615
interface SmokeCase {
716
element: string;
817
view: ViewKind;
18+
strategy: StrategyKind;
19+
hasSession: boolean;
920
url: string;
1021
}
1122

@@ -36,52 +47,84 @@ const IGNORE_CONSOLE_PATTERNS = [
3647
/The pseudo class ":nth-child" is potentially unsafe/i,
3748
];
3849

50+
const MATRIX_STRATEGIES = (process.env.MATRIX_STRATEGIES?.trim() || 'esm')
51+
.split(',')
52+
.map((value) => value.trim())
53+
.filter((value): value is StrategyKind => value === 'esm' || value === 'iife');
54+
3955
function buildCases(): SmokeCase[] {
4056
const cases: SmokeCase[] = [];
41-
for (const element of ELEMENT_REGISTRY) {
42-
cases.push({
43-
element: element.name,
44-
view: 'deliver',
45-
url: `/${element.name}/deliver?mode=gather&role=student&player=iife`,
46-
});
47-
if (element.hasAuthor) {
48-
cases.push({
49-
element: element.name,
50-
view: 'author',
51-
url: `/${element.name}/author?demo=default&player=iife`,
52-
});
53-
}
54-
if (element.hasPrint) {
55-
cases.push({
56-
element: element.name,
57-
view: 'print',
58-
url: `/${element.name}/print?role=student&player=iife`,
59-
});
60-
}
61-
62-
// Keep an explicit regression case for known number-line IIFE/runtime interactions.
63-
if (element.name === 'number-line') {
57+
for (const strategy of MATRIX_STRATEGIES) {
58+
for (const element of ELEMENT_REGISTRY) {
6459
cases.push({
6560
element: element.name,
6661
view: 'deliver',
67-
url: `/${element.name}/deliver?demo=basic-points&mode=gather&role=student&player=iife`,
62+
strategy,
63+
hasSession: element.hasSession,
64+
url: `/${element.name}/deliver?mode=gather&role=student&player=${strategy}`,
6865
});
66+
if (element.hasAuthor) {
67+
cases.push({
68+
element: element.name,
69+
view: 'author',
70+
strategy,
71+
hasSession: element.hasSession,
72+
url: `/${element.name}/author?demo=default&player=${strategy}`,
73+
});
74+
}
75+
if (element.hasPrint) {
76+
cases.push({
77+
element: element.name,
78+
view: 'print',
79+
strategy,
80+
hasSession: element.hasSession,
81+
url: `/${element.name}/print?role=student&player=${strategy}`,
82+
});
83+
}
84+
85+
// Keep an explicit regression case for known number-line IIFE/runtime interactions.
86+
if (element.name === 'number-line' && strategy === 'iife') {
87+
cases.push({
88+
element: element.name,
89+
view: 'deliver',
90+
strategy,
91+
hasSession: element.hasSession,
92+
url: `/${element.name}/deliver?demo=basic-points&mode=gather&role=student&player=${strategy}`,
93+
});
94+
}
6995
}
7096
}
7197
return cases;
7298
}
7399

74-
async function waitForIifeSettle(page: Page, view: ViewKind, timeoutMs = 20_000) {
75-
await page.waitForFunction(
76-
() => {
77-
const iifeLoading = Array.from(document.querySelectorAll('.loading')).some((node) =>
78-
/IIFE/i.test(node.textContent || '')
79-
);
80-
return !iifeLoading;
81-
},
82-
undefined,
83-
{ timeout: timeoutMs }
84-
);
100+
async function waitForStrategySettle(
101+
page: Page,
102+
view: ViewKind,
103+
strategy: StrategyKind,
104+
timeoutMs = 20_000
105+
) {
106+
if (strategy === 'iife') {
107+
await page.waitForFunction(
108+
() => {
109+
const iifeLoading = Array.from(document.querySelectorAll('.loading')).some((node) =>
110+
/IIFE/i.test(node.textContent || '')
111+
);
112+
return !iifeLoading;
113+
},
114+
undefined,
115+
{ timeout: timeoutMs }
116+
);
117+
} else {
118+
await page.waitForFunction(
119+
() => {
120+
const loading = document.querySelector('.loading');
121+
const error = document.querySelector('.error');
122+
return !loading && !error;
123+
},
124+
undefined,
125+
{ timeout: timeoutMs }
126+
);
127+
}
85128

86129
// Route-specific UI markers prove the view actually rendered.
87130
if (view === 'deliver') {
@@ -106,16 +149,111 @@ function findCriticalConsole(messages: string[]): string[] {
106149
});
107150
}
108151

109-
test.describe('IIFE smoke matrix across PIE elements', () => {
152+
const EVALUATE_SIGNAL_SELECTOR =
153+
'[data-testid="score-value"], [data-testid="scoring-panel"], [data-testid="show-correct-answer"], button:has-text("Show correct answer"), button:has-text("Hide correct answer")';
154+
155+
const REQUIRE_EVALUATE_SIGNAL_ELEMENTS = new Set([
156+
'multiple-choice',
157+
'ebsr',
158+
'matrix',
159+
'match',
160+
'likert',
161+
'inline-dropdown',
162+
'select-text',
163+
'math-inline',
164+
'math-templated',
165+
]);
166+
167+
const REQUIRE_SESSION_MUTATION_ELEMENTS = new Set(['multiple-choice', 'ebsr']);
168+
169+
async function synthesizeSessionChanged(page: Page): Promise<boolean> {
170+
return await page.evaluate((token) => {
171+
const host = document.querySelector('pie-element-player') as any;
172+
if (!(host instanceof HTMLElement)) {
173+
return false;
174+
}
175+
const innerElement =
176+
host.querySelector('.demo-element-player > *:not(.loading):not(.error)') ??
177+
host.querySelector('.element-container > *:not(.loading):not(.error)');
178+
if (!(innerElement instanceof HTMLElement)) {
179+
return false;
180+
}
181+
const currentSession =
182+
host.session && typeof host.session === 'object'
183+
? JSON.parse(JSON.stringify(host.session))
184+
: {};
185+
const nextSession = { ...currentSession, __matrixMarker: token };
186+
innerElement.dispatchEvent(
187+
new CustomEvent('session-changed', {
188+
detail: { session: nextSession },
189+
bubbles: true,
190+
composed: true,
191+
})
192+
);
193+
return true;
194+
}, `matrix-${Date.now()}`);
195+
}
196+
197+
async function verifyDeliveryInteractionAndEvaluate(
198+
page: Page,
199+
item: SmokeCase
200+
): Promise<string | null> {
201+
if (!item.hasSession) {
202+
return null;
203+
}
204+
205+
const root = deliveryContainer(page);
206+
try {
207+
await root.waitFor({ state: 'visible', timeout: 15_000 });
208+
} catch (err: any) {
209+
return `Delivery root not visible: ${err?.message || String(err)}`;
210+
}
211+
212+
const before = await getSessionState(page);
213+
214+
try {
215+
await interactOnce(page, root);
216+
} catch (err: any) {
217+
const fallbackDispatched = await synthesizeSessionChanged(page);
218+
if (!fallbackDispatched) {
219+
return `No interactive control found: ${err?.message || String(err)}`;
220+
}
221+
}
222+
223+
const after = await waitForSessionMutation(page, before, 10_000);
224+
if (
225+
REQUIRE_SESSION_MUTATION_ELEMENTS.has(item.element) &&
226+
JSON.stringify(after ?? {}) === JSON.stringify(before ?? {})
227+
) {
228+
return 'Session did not mutate after delivery interaction';
229+
}
230+
231+
try {
232+
await switchRole(page, 'instructor');
233+
await switchMode(page, 'evaluate');
234+
if (REQUIRE_EVALUATE_SIGNAL_ELEMENTS.has(item.element)) {
235+
await page
236+
.locator(EVALUATE_SIGNAL_SELECTOR)
237+
.first()
238+
.waitFor({ state: 'visible', timeout: 15_000 });
239+
}
240+
} catch (err: any) {
241+
return `Evaluate/correct-answer signal not visible: ${err?.message || String(err)}`;
242+
}
243+
244+
return null;
245+
}
246+
247+
test.describe('Strategy smoke matrix across PIE elements', () => {
110248
test('all elements/views render without critical runtime or build failures', async ({ page }) => {
111249
test.setTimeout(15 * 60 * 1000);
112250

113251
const failures: SmokeFailure[] = [];
114252
const matrix = buildCases();
115253

116254
for (const item of matrix) {
117-
await test.step(`${item.element} :: ${item.view}`, async () => {
118-
console.log(`[smoke] checking ${item.element} :: ${item.view}`);
255+
await test.step(`${item.element} :: ${item.view} :: ${item.strategy}`, async () => {
256+
console.log(`[smoke] checking ${item.element} :: ${item.view} :: ${item.strategy}`);
119257
const consoleMessages: string[] = [];
120258
const pageErrors: string[] = [];
121259

@@ -134,7 +272,7 @@ test.describe('IIFE smoke matrix across PIE elements', () => {
134272

135273
try {
136274
await page.goto(item.url, { waitUntil: 'domcontentloaded' });
137-
await waitForIifeSettle(page, item.view);
275+
await waitForStrategySettle(page, item.view, item.strategy);
138276

139277
const errorNodes = page.locator('.error');
140278
const errorCount = await errorNodes.count();
@@ -170,6 +308,19 @@ test.describe('IIFE smoke matrix across PIE elements', () => {
170308
reason: 'Critical console/runtime errors',
171309
details: criticalConsole.slice(0, 6).join('\n'),
172310
});
311+
return;
312+
}
313+
314+
if (item.view === 'deliver') {
315+
const verifyError = await verifyDeliveryInteractionAndEvaluate(page, item);
316+
if (verifyError) {
317+
failures.push({
318+
element: item.element,
319+
view: item.view,
320+
url: item.url,
321+
reason: verifyError,
322+
});
323+
}
173324
}
174325
} catch (err: any) {
175326
failures.push({

bun.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/elements-react/ebsr/src/author/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import React from 'react';
1212
import { createRoot } from 'react-dom/client';
1313
import { ModelUpdatedEvent } from '@pie-element/shared-configure-events';
14-
import MultipleChoiceConfigure from '@pie-element/multiple-choice';
14+
import MultipleChoiceConfigure from '@pie-element/multiple-choice/author';
1515
import { defaults } from 'lodash-es';
1616
import Main from './main.js';
1717

tools/cli/src/lib/upstream/sync-imports.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,19 @@ export function transformSharedPackageImports(content: string): string {
526526
}
527527

528528
/**
529-
* Rewrite legacy configure subpath imports to package roots.
529+
* Rewrite legacy configure subpath imports to author entrypoints.
530530
*
531531
* Upstream sometimes imports configure elements via:
532532
* - @pie-element/<element-name>/configure/lib
533533
*
534-
* In this repo, package exports do not expose that subpath. Importing the package root
535-
* resolves correctly via the package `exports` map and works for demo/Vite resolution.
534+
* In this repo, configure code is synced into `src/author` and packages expose that via
535+
* the `./author` export. Rewriting to `/author` preserves author-vs-delivery boundaries.
536536
*/
537537
export function transformLegacyConfigureLibImports(content: string): string {
538-
return content.replace(/from\s+['"](@pie-element\/[^/'"]+)\/configure\/lib['"]/g, "from '$1'");
538+
return content.replace(
539+
/from\s+['"](@pie-element\/[^/'"]+)\/configure\/lib['"]/g,
540+
"from '$1/author'"
541+
);
539542
}
540543

541544
/**

tools/cli/tests/transform-configure-lib-imports.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ import {
66
} from '../src/lib/upstream/sync-imports';
77

88
describe('transformLegacyConfigureLibImports', () => {
9-
it('rewrites @pie-element configure/lib imports to package roots', () => {
9+
it('rewrites @pie-element configure/lib imports to author entrypoints', () => {
1010
const input = `
1111
import RubricConfigure from '@pie-element/rubric/configure/lib';
1212
import MultiTraitRubricConfigure from "@pie-element/multi-trait-rubric/configure/lib";
1313
`;
1414

1515
const output = transformLegacyConfigureLibImports(input);
1616

17-
expect(output).toContain("import RubricConfigure from '@pie-element/rubric';");
17+
expect(output).toContain("import RubricConfigure from '@pie-element/rubric/author';");
1818
expect(output).toContain(
19-
"import MultiTraitRubricConfigure from '@pie-element/multi-trait-rubric';"
19+
"import MultiTraitRubricConfigure from '@pie-element/multi-trait-rubric/author';"
2020
);
2121
});
2222

0 commit comments

Comments
 (0)