diff --git a/apps/learnosity-parity-demo/src/lib/samples/mc-populated-blank.json b/apps/learnosity-parity-demo/src/lib/samples/mc-populated-blank.json index 18c960a4..ca5b0039 100644 --- a/apps/learnosity-parity-demo/src/lib/samples/mc-populated-blank.json +++ b/apps/learnosity-parity-demo/src/lib/samples/mc-populated-blank.json @@ -819,6 +819,56 @@ "choiceId": "" } }, + { + "id": "variant-sel-vic-cloze-punct", + "title": "sel_vic — cloze followed by punctuation (CONTOOL-2572)", + "description": "Reproduction sample for CONTOOL-2572: cloze marker is immediately followed by a period; PIE renders a different gap to the punctuation than Learnosity. Sourced from item 0d6e94d3-b8d7-486c-803f-b037e49bbd9c.", + "tags": ["mc-populated-blank", "cqt-sample", "sel_vic", "CONTOOL-2572"], + "model": { + "id": "18b", + "element": "mc-populated-blank", + "prompt": "", + "promptEnabled": false, + "template": "

Choose the right word: {{blank}}.

", + "choiceMode": "text", + "choices": [ + { + "id": "distractor_1", + "labelHtml": "run" + }, + { + "id": "distractor_2", + "labelHtml": "do" + }, + { + "id": "distractor_3", + "labelHtml": "toy" + } + ], + "correctChoiceId": "distractor_1", + "hasAudio": true, + "audioUrl": "https://assets.learnosity.com/organisations/844/af9f6179-fca4-4e5a-9b7f-8fb5374a23e5.mp3", + "audioTranscript": "Choose the right word.", + "interactionMode": "populate_blank", + "sentenceHtml": "", + "layoutProfile": "inline_sentence", + "customType": "sel_vic", + "useFeatureButtonAudio": true, + "choiceLayout": "vertical", + "locale": "", + "autoplayAudioEnabled": true, + "completeAudioEnabled": true, + "source": { + "csvQuestionTypeTag": "sel_vicv0.0.1", + "learnosityItemReference": "0d6e94d3-b8d7-486c-803f-b037e49bbd9c_v2.0" + } + }, + "session": { + "id": "18b", + "element": "mc-populated-blank", + "choiceId": "" + } + }, { "id": "variant-sel-r1-gg-plus-graphic", "title": "sel_r1-gg_plusggg \u2014 graphic choices English (bc827dec)", diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gg-plus-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gg-plus-parity.spec.ts index a2915436..718bfef4 100644 --- a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gg-plus-parity.spec.ts +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gg-plus-parity.spec.ts @@ -119,18 +119,19 @@ test('gg-plus: hovered unselected choice tile background is #f2f2f2', async ({ p }); // --------------------------------------------------------------------------- -// 5. Template-line font size is 1.9em (r1 content-element base) -// r1.scss: .rli-r1-content-element { font-size: 1.9em } — shared via sel-r1-base.css. -// Without the base CSS this variant renders template tokens at the default body size. +// 5. Template-line font size resolves to 26.6px (r1 content-element base) +// r1.scss: .rli-r1-content-element { font-size: 1.9em } against the Learnosity +// host base of 14px → 26.6px. PIE pins the same 14px base on the variant root +// in sel-r1-base.css so the existing 1.9em rule resolves to the same px value. +// Tolerance ±0.5px to absorb sub-pixel font rendering noise. // --------------------------------------------------------------------------- -test('gg-plus: template line font size is 1.9em', async ({ page }) => { +test('gg-plus: template line font size matches LSY (14px × 1.9em = 26.6px)', async ({ page }) => { await openGgPlusRoute(page); const root = deliveryContainer(page); const templateLine = root.locator('.pie-template-line'); await expect(templateLine).toBeVisible(); const px = parseFloat(await templateLine.evaluate((el) => getComputedStyle(el).fontSize)); - expect(px).toBeGreaterThanOrEqual(30); - expect(px).toBeLessThanOrEqual(32); + expect(Math.abs(px - 26.6)).toBeLessThanOrEqual(0.5); }); // --------------------------------------------------------------------------- diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-ggplus-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-ggplus-parity.spec.ts index 3cda040e..1a350696 100644 --- a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-ggplus-parity.spec.ts +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-ggplus-parity.spec.ts @@ -119,17 +119,17 @@ test('ggplus: hovered unselected choice tile background is #f2f2f2', async ({ pa }); // --------------------------------------------------------------------------- -// 5. Template-line font size is 1.9em (r1 content-element base) -// r1.scss: .rli-r1-content-element { font-size: 1.9em } — shared via sel-r1-base.css. +// 5. Template-line font size resolves to 26.6px (r1 content-element base) +// r1.scss: .rli-r1-content-element { font-size: 1.9em } against the Learnosity +// host base of 14px → 26.6px. Tolerance ±0.5px for sub-pixel rendering. // --------------------------------------------------------------------------- -test('ggplus: template line font size is 1.9em', async ({ page }) => { +test('ggplus: template line font size matches LSY (14px × 1.9em = 26.6px)', async ({ page }) => { await openGgplusRoute(page); const root = deliveryContainer(page); const templateLine = root.locator('.pie-template-line'); await expect(templateLine).toBeVisible(); const px = parseFloat(await templateLine.evaluate((el) => getComputedStyle(el).fontSize)); - expect(px).toBeGreaterThanOrEqual(30); - expect(px).toBeLessThanOrEqual(32); + expect(Math.abs(px - 26.6)).toBeLessThanOrEqual(0.5); }); // --------------------------------------------------------------------------- diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gplusggg-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gplusggg-parity.spec.ts index df69d9f9..88c6c1af 100644 --- a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gplusggg-parity.spec.ts +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-gplusggg-parity.spec.ts @@ -215,12 +215,11 @@ test('gplusggg: after-cloze content does not shift vertically when a distractor // --------------------------------------------------------------------------- // r1.scss: .rli-r1-content-element { font-size: 1.9em } wraps all stem tokens. -// Inline font-size spans in the template (e.g. style="font-size:1.8em") must -// multiply on top of the 1.9em base — so the template-line context must be 1.9em. +// Inline font-size spans in the template (e.g. style="font-size:1.8em") multiply +// on top of this. The 1.9em resolves against the Learnosity host base of 14px, +// pinned on the variant root in sel-r1-base.css for parity. → 26.6px. // --------------------------------------------------------------------------- -test('gplusggg: template line font size is 1.9em (r1 content-element base size)', async ({ - page, -}) => { +test('gplusggg: template line font size matches LSY (14px × 1.9em = 26.6px)', async ({ page }) => { await openGplusgggRoute(page); const root = deliveryContainer(page); @@ -228,10 +227,8 @@ test('gplusggg: template line font size is 1.9em (r1 content-element base size)' await expect(templateLine).toBeVisible(); const fontSize = await templateLine.evaluate((el) => getComputedStyle(el).fontSize); - // 1.9em relative to default 16px body = 30.4px; accept 30–31px to allow subpixel rounding. const px = parseFloat(fontSize); - expect(px).toBeGreaterThanOrEqual(30); - expect(px).toBeLessThanOrEqual(32); + expect(Math.abs(px - 26.6)).toBeLessThanOrEqual(0.5); }); // --------------------------------------------------------------------------- diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-graphic-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-graphic-parity.spec.ts index e1a8c77f..36d896af 100644 --- a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-graphic-parity.spec.ts +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-graphic-parity.spec.ts @@ -19,7 +19,7 @@ */ import { type Page, expect, test } from '@playwright/test'; -import { deliveryContainer, waitForMathRendering } from './test-helpers'; +import { deliveryContainer, disableTransitions, waitForMathRendering } from './test-helpers'; import { PARITY_REGIONS } from './parity-regions'; const GRAPHIC_VARIANTS = [ @@ -47,6 +47,10 @@ async function openGraphicRoute(page: Page, demoId: string) { }, { timeout: 30_000 } ); + // Kill CSS transitions so toHaveCSS doesn't sample mid-fade. ChoiceRow.svelte + // animates background-color over 120ms; without this, a check() followed by + // an immediate computed-style read can land on the intermediate color. + await disableTransitions(page); } for (const DEMO_ID of GRAPHIC_VARIANTS) { @@ -72,7 +76,6 @@ for (const DEMO_ID of GRAPHIC_VARIANTS) { const root = deliveryContainer(page); await root.locator('input[type="radio"]').first().check(); - await page.waitForTimeout(100); const selectedRow = root.locator('.pie-choice.is-selected').first(); await expect(selectedRow).toBeVisible(); @@ -105,10 +108,9 @@ for (const DEMO_ID of GRAPHIC_VARIANTS) { const bgBefore = await firstTile.evaluate((el) => getComputedStyle(el).backgroundColor); await firstTile.hover(); - await page.waitForTimeout(100); - const bgAfter = await firstTile.evaluate((el) => getComputedStyle(el).backgroundColor); - - expect(bgAfter).not.toBe(bgBefore); + await expect + .poll(() => firstTile.evaluate((el) => getComputedStyle(el).backgroundColor)) + .not.toBe(bgBefore); }); test('gap between adjacent choice tiles on the same row is at least 16px', async ({ page }) => { diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2567-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2567-parity.spec.ts new file mode 100644 index 00000000..36682b4f --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2567-parity.spec.ts @@ -0,0 +1,102 @@ +/** + * Live parity test for CONTOOL-2567: + * https://illuminate.atlassian.net/browse/CONTOOL-2567 + * + * Reported by content: McPopulatedBlank renders larger text than the + * Learnosity reference, and the rendered text runs together in the + * distractors and in the stem after a selection is made. + * + * Root cause hypothesis: McPopulatedBlank's stem/distractor cascade is + * em-based, while the Learnosity Custom Question type sizes the equivalent + * spans in px. Author content uses inline `font-size:1.8em;` on the same + * spans on both sides — Learnosity's px-based parents resolve the em + * relative to a smaller base, so the visible glyph is ~14% smaller than PIE's. + * + * The test asserts that the visible 1.8em span inside the stem and the first + * distractor renders at the same computed font-size as the corresponding + * Learnosity span, within ±0.5px to absorb sub-pixel rendering noise. + * + * Variant: variant-sel-r1-g-stem (token_sequence layout, horizontal choices). + * The reported reference item (1d45e2d4-...) is a different item of the same + * customType; the bug is at the customType layer, not item-content specific. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-r1-g-stem'; +const FONT_SIZE_TOLERANCE_PX = 0.5; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +/** + * Find the deepest descendant of `root` whose own (non-nested) text content + * includes `text`. Used to locate the leaf that + * directly wraps the visible token, regardless of intervening wrappers. + */ +function deepestTextSpanFn() { + return (args: { rootSelector: string; text: string }) => { + const root = document.querySelector(args.rootSelector); + if (!root) return null; + let candidate: Element | null = null; + const walk = (el: Element) => { + const ownText = Array.from(el.childNodes) + .filter((n) => n.nodeType === Node.TEXT_NODE) + .map((n) => n.textContent ?? '') + .join(''); + if (ownText.includes(args.text)) candidate = el; + for (const child of Array.from(el.children)) walk(child); + }; + walk(root); + if (!candidate) return null; + return parseFloat(getComputedStyle(candidate).fontSize); + }; +} + +test.describe('CONTOOL-2567 — font-size parity', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('stem token (1.8em span) matches Learnosity computed font-size', async ({ page }) => { + await openParityRoute(page); + + const piePx = await page.evaluate(deepestTextSpanFn(), { + rootSelector: '#pie-container .pie-template-line', + text: 'will', + }); + const lrnPx = await page.evaluate(deepestTextSpanFn(), { + rootSelector: '#learnosity-container .rli-r1-stem', + text: 'will', + }); + + expect(piePx, 'PIE stem span not found').not.toBeNull(); + expect(lrnPx, 'Learnosity stem span not found').not.toBeNull(); + expect(Math.abs((piePx as number) - (lrnPx as number))).toBeLessThanOrEqual( + FONT_SIZE_TOLERANCE_PX + ); + }); + + test('first distractor (1.8em span) matches Learnosity computed font-size', async ({ page }) => { + await openParityRoute(page); + + const piePx = await page.evaluate(deepestTextSpanFn(), { + rootSelector: '#pie-container .pie-choices-fieldset .pie-choice', + text: 'fill', + }); + const lrnPx = await page.evaluate(deepestTextSpanFn(), { + rootSelector: '#learnosity-container .rli-r1-distractors .rli-r1-distractor', + text: 'fill', + }); + + expect(piePx, 'PIE distractor span not found').not.toBeNull(); + expect(lrnPx, 'Learnosity distractor span not found').not.toBeNull(); + expect(Math.abs((piePx as number) - (lrnPx as number))).toBeLessThanOrEqual( + FONT_SIZE_TOLERANCE_PX + ); + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2569-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2569-parity.spec.ts new file mode 100644 index 00000000..c4a7671e --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2569-parity.spec.ts @@ -0,0 +1,85 @@ +/** + * Live parity test for CONTOOL-2569: + * https://illuminate.atlassian.net/browse/CONTOOL-2569 + * + * Reported by content: when a distractor is selected on the + * sel_r1-_ggplusggg variant, the visible whitespace between the cloze + * marker (filled blank) and the first trailing stem token does not match + * the Learnosity reference. PIE renders a noticeably larger gap than LSY. + * + * Variant: variant-sel-r1-ggplus (token_sequence layout, horizontal choices). + * The reported reference item (f522383b-8f50-4014-b557-f1b1acd678f3) is a + * different sample of the same customType; the gap divergence is at the + * customType layer, not item-content specific. + * + * The test asserts that the horizontal gap from the cloze marker's right + * edge to the first trailing stem token's left edge matches the same gap + * on the Learnosity side, within ±2px to absorb sub-pixel rendering noise. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-r1-ggplus'; +const GAP_TOLERANCE_PX = 2; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +/** + * Measures the horizontal gap (in px) between the cloze marker's right edge + * and the first trailing stem token's left edge on the given side. The + * trailing token is the first descendant of the stem container that is NOT + * inside the cloze marker itself — Learnosity wraps trailing tokens in + * .rli-r1-content-element (siblings of .rli-r1-cloze), and PIE renders them + * as direct-child spans of .pie-template-line outside .pie-blank-slot. + */ +function measureGapFn() { + return (args: { stemSelector: string; clozeSelector: string; tokenSelector: string }) => { + const stem = document.querySelector(args.stemSelector) as HTMLElement | null; + const cloze = document.querySelector(args.clozeSelector) as HTMLElement | null; + if (!stem || !cloze) return null; + const tokenCandidates = Array.from(stem.querySelectorAll(args.tokenSelector)) as HTMLElement[]; + const token = tokenCandidates.find((el) => !cloze.contains(el)); + if (!token) return null; + const clozeRight = cloze.getBoundingClientRect().right; + const tokenLeft = token.getBoundingClientRect().left; + return tokenLeft - clozeRight; + }; +} + +test.describe('CONTOOL-2569 — cloze-to-token gap parity (ggplus)', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('cloze→trailing-token gap matches Learnosity within ±2px after selection', async ({ + page, + }) => { + await openParityRoute(page); + + // Select the first distractor on each side so the cloze renders a value. + await page.locator('#pie-container input[type="radio"]').first().check(); + await page.locator('#learnosity-container input[type="radio"]').first().check(); + await page.waitForTimeout(200); + + const piePx = await page.evaluate(measureGapFn(), { + stemSelector: '#pie-container .pie-template-line', + clozeSelector: '#pie-container .pie-blank-slot', + // Direct-child classless spans: trailing tokens from {@html} template content. + tokenSelector: ':scope > span:not([class])', + }); + const lrnPx = await page.evaluate(measureGapFn(), { + stemSelector: '#learnosity-container .rli-r1-stem', + clozeSelector: '#learnosity-container .rli-r1-cloze', + tokenSelector: '.rli-r1-content-element', + }); + + expect(piePx, 'PIE cloze/token not found').not.toBeNull(); + expect(lrnPx, 'Learnosity cloze/token not found').not.toBeNull(); + expect(Math.abs((piePx as number) - (lrnPx as number))).toBeLessThanOrEqual(GAP_TOLERANCE_PX); + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2570-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2570-parity.spec.ts new file mode 100644 index 00000000..fc1a8cc8 --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2570-parity.spec.ts @@ -0,0 +1,96 @@ +/** + * Live parity test for CONTOOL-2570: + * https://illuminate.atlassian.net/browse/CONTOOL-2570 + * + * Reported by content: on every plusggg variant that uses image content + * (graphic stem tokens or graphic distractors), images render smaller in PIE + * than they do in Learnosity. The reported reference item is + * 69d73376-ab3c-4ec6-8453-5b4e88e8977a (sel_r1-_ggplusggg, graphic). + * + * Variant: variant-sel-r1-ggplus-graphic — stem has two graphic tokens (woman, + * girl) and three graphic distractors. Authored width/height attributes are + * 108×108. LSY normalizes these to 150×150 (it rewrites the attributes server + * side, and r1.scss applies .rli-r1-content-element { min/max:150px }), so + * every image in the stem and the choice tiles renders at 150×150. + * + * The test asserts PIE renders the same image size as LSY, within ±2px to + * absorb sub-pixel rendering noise. Without the fix PIE renders the authored + * 108×108 box, which is the visible regression. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-r1-ggplus-graphic'; +const SIZE_TOLERANCE_PX = 2; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +function imageSizesFn() { + return (selector: string) => { + const imgs = Array.from(document.querySelectorAll(selector)) as HTMLImageElement[]; + return imgs.map((img) => { + const rect = img.getBoundingClientRect(); + return { width: rect.width, height: rect.height }; + }); + }; +} + +test.describe('CONTOOL-2570 — graphic image size parity (ggplus-graphic)', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('stem graphic images render at the same size as Learnosity within ±2px', async ({ + page, + }) => { + await openParityRoute(page); + + const pieSizes = await page.evaluate(imageSizesFn(), '#pie-container .pie-template-line img'); + const lrnSizes = await page.evaluate(imageSizesFn(), '#learnosity-container .rli-r1-stem img'); + + expect(pieSizes.length, 'PIE stem images').toBeGreaterThan(0); + expect(pieSizes.length, 'PIE/LSY stem image count mismatch').toBe(lrnSizes.length); + + for (let i = 0; i < pieSizes.length; i += 1) { + expect( + Math.abs(pieSizes[i].width - lrnSizes[i].width), + `stem image #${i} width drift` + ).toBeLessThanOrEqual(SIZE_TOLERANCE_PX); + expect( + Math.abs(pieSizes[i].height - lrnSizes[i].height), + `stem image #${i} height drift` + ).toBeLessThanOrEqual(SIZE_TOLERANCE_PX); + } + }); + + test('distractor graphic images render at the same size as Learnosity within ±2px', async ({ + page, + }) => { + await openParityRoute(page); + + const pieSizes = await page.evaluate(imageSizesFn(), '#pie-container .pie-choice-label img'); + const lrnSizes = await page.evaluate( + imageSizesFn(), + '#learnosity-container .rli-r1-distractors img' + ); + + expect(pieSizes.length, 'PIE distractor images').toBeGreaterThan(0); + expect(pieSizes.length, 'PIE/LSY distractor image count mismatch').toBe(lrnSizes.length); + + for (let i = 0; i < pieSizes.length; i += 1) { + expect( + Math.abs(pieSizes[i].width - lrnSizes[i].width), + `distractor image #${i} width drift` + ).toBeLessThanOrEqual(SIZE_TOLERANCE_PX); + expect( + Math.abs(pieSizes[i].height - lrnSizes[i].height), + `distractor image #${i} height drift` + ).toBeLessThanOrEqual(SIZE_TOLERANCE_PX); + } + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2571-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2571-parity.spec.ts new file mode 100644 index 00000000..2e3d9bc9 --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2571-parity.spec.ts @@ -0,0 +1,79 @@ +/** + * Live parity test for CONTOOL-2571: + * https://illuminate.atlassian.net/browse/CONTOOL-2571 + * + * Reported by content: distractor labels and the filled cloze on the + * sel_r1-s3_plusggg variant render in a different weight in PIE than they do + * in Learnosity. The reported reference item is + * b924b8b7-0ce2-4a03-bf10-3f9647e23025. + * + * Variant: variant-sel-r1-s3 (stimulus_image_blank, horizontal text choices, + * distractor labels wrapped in ). LSY's + * sel_r1-s3_plusggg/question.css has no font-weight rule for distractors or + * the cloze content-element, so both fall through to the host root weight 400. + * PIE adds a font-weight:500 rule on .pie-choice-label that does not exist in + * LSY, so PIE renders the same content visibly heavier. + * + * The test reads the computed font-weight of the deepest text-bearing span + * (the inner author span) on both sides and asserts they match. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-r1-s3'; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +function fontWeightOf() { + return (selector: string) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) return null; + return getComputedStyle(el).fontWeight; + }; +} + +test.describe('CONTOOL-2571 — distractor/cloze font weight parity (s3)', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('distractor inner span font weight matches Learnosity', async ({ page }) => { + await openParityRoute(page); + + // The inner author carries the visible text + // on both sides; if PIE applies any weight override on its ancestor, the + // inner span inherits it. + const piePartialSel = '#pie-container .pie-choice-label span[style*="font-size"]'; + const lrnPartialSel = '#learnosity-container .rli-s3-content-element span[style*="font-size"]'; + const pieWeight = await page.evaluate(fontWeightOf(), piePartialSel); + const lrnWeight = await page.evaluate(fontWeightOf(), lrnPartialSel); + + expect(pieWeight, 'PIE distractor inner span').not.toBeNull(); + expect(lrnWeight, 'LSY distractor inner span').not.toBeNull(); + expect(pieWeight).toBe(lrnWeight); + }); + + test('filled cloze inner span font weight matches Learnosity', async ({ page }) => { + await openParityRoute(page); + + // Select the first distractor on each side so the cloze fills with the + // selected label; only then does the filled cloze span appear. + await page.locator('#pie-container input[type="radio"]').first().check(); + await page.locator('#learnosity-container input[type="radio"]').first().check(); + await page.waitForTimeout(200); + + const pieClozeSel = '#pie-container .pie-blank-value span[style*="font-size"]'; + const lrnClozeSel = '#learnosity-container .rli-s3-cloze span[style*="font-size"]'; + const pieWeight = await page.evaluate(fontWeightOf(), pieClozeSel); + const lrnWeight = await page.evaluate(fontWeightOf(), lrnClozeSel); + + expect(pieWeight, 'PIE filled cloze inner span').not.toBeNull(); + expect(lrnWeight, 'LSY filled cloze inner span').not.toBeNull(); + expect(pieWeight).toBe(lrnWeight); + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2572-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2572-parity.spec.ts new file mode 100644 index 00000000..863b527c --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2572-parity.spec.ts @@ -0,0 +1,111 @@ +/** + * Live parity test for CONTOOL-2572: + * https://illuminate.atlassian.net/browse/CONTOOL-2572 + * + * Reported by content: on the sel_vic variant, when the cloze marker is + * immediately followed by a piece of punctuation (e.g. a period), the + * horizontal gap between the filled cloze value and the punctuation is not + * the same in PIE as it is in Learnosity. Reported reference item is + * 0d6e94d3-b8d7-486c-803f-b037e49bbd9c. + * + * The reproduction sample registered for this case is `variant-sel-vic-cloze-punct`, + * which uses a generic `Choose the right word: {{blank}}.` template so the + * leading text doesn't affect the cloze→period adjacency we are measuring. + * + * The test selects a distractor on each side, then measures the px gap from + * the cloze marker's right edge to the leading edge of the trailing + * punctuation node. PIE and LSY must agree within ±2px. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-vic-cloze-punct'; +const GAP_TOLERANCE_PX = 2; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +/** + * Returns the horizontal gap (in CSS px) from the right edge of the cloze + * marker to the leading edge of the first trailing punctuation glyph. We use + * a Range over the trailing text node so the measurement is glyph-accurate + * rather than including any container padding/margin past the punctuation. + */ +function measureClozeToPunctGapFn() { + return (args: { stemSelector: string; clozeSelector: string }) => { + const stem = document.querySelector(args.stemSelector) as HTMLElement | null; + const cloze = document.querySelector(args.clozeSelector) as HTMLElement | null; + if (!stem || !cloze) return null; + const clozeRect = cloze.getBoundingClientRect(); + + // Find the first punctuation glyph that sits to the right of the cloze on + // the same line. We walk every text node in the stem (skipping the cloze + // contents themselves) and for each punctuation char build a Range and + // measure its rect. + const walker = document.createTreeWalker(stem, NodeFilter.SHOW_TEXT); + let best: { left: number; rect: DOMRect } | null = null; + let node = walker.nextNode() as Text | null; + while (node) { + if (cloze.contains(node)) { + node = walker.nextNode() as Text | null; + continue; + } + const text = node.nodeValue ?? ''; + for (let i = 0; i < text.length; i += 1) { + if (!/[.,!?;:]/.test(text[i])) continue; + const range = document.createRange(); + range.setStart(node, i); + range.setEnd(node, i + 1); + const rect = range.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) continue; + // Same-line: punctuation top is within the cloze's vertical extent. + const sameLine = rect.top < clozeRect.bottom - 1 && rect.bottom > clozeRect.top + 1; + if (!sameLine) continue; + // Must be to the right of the cloze. + if (rect.left < clozeRect.right) continue; + if (!best || rect.left < best.left) { + best = { left: rect.left, rect }; + } + } + node = walker.nextNode() as Text | null; + } + if (!best) return null; + return best.left - clozeRect.right; + }; +} + +test.describe('CONTOOL-2572 — sel-vic cloze→punctuation gap parity', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('cloze→trailing-period gap matches Learnosity within ±2px after selection', async ({ + page, + }) => { + await openParityRoute(page); + + await page.locator('#pie-container input[type="radio"]').first().check(); + await page.locator('#learnosity-container input[type="radio"]').first().check(); + await page.waitForTimeout(200); + + const piePx = await page.evaluate(measureClozeToPunctGapFn(), { + stemSelector: '#pie-container .pie-template-line', + clozeSelector: '#pie-container .pie-blank-slot', + }); + const lrnPx = await page.evaluate(measureClozeToPunctGapFn(), { + stemSelector: '#learnosity-container .rli-vic-stem, #learnosity-container .rli-vic-answer', + clozeSelector: '#learnosity-container .rli-vic-cloze', + }); + + expect(piePx, 'PIE cloze/punct not found').not.toBeNull(); + expect(lrnPx, 'Learnosity cloze/punct not found').not.toBeNull(); + expect( + Math.abs((piePx as number) - (lrnPx as number)), + `PIE gap=${piePx} LSY gap=${lrnPx}` + ).toBeLessThanOrEqual(GAP_TOLERANCE_PX); + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2573-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2573-parity.spec.ts new file mode 100644 index 00000000..04886ee3 --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2573-parity.spec.ts @@ -0,0 +1,157 @@ +/** + * Live parity test for CONTOOL-2573: + * https://illuminate.atlassian.net/browse/CONTOOL-2573 + * + * Reported by content: on the sel_vic variant, when the viewport is narrow + * enough that the template sentence wraps to a second line, PIE renders the + * second line center-aligned while Learnosity renders it left-aligned. The + * reported reference item is 0d6e94d3-b8d7-486c-803f-b037e49bbd9c (v2.0). + * + * The reproduction reuses the variant-sel-vic-cloze-punct sample (registered + * for CONTOOL-2572) but extends the template via JS to a length that forces a + * wrap at the test viewport. The test then measures the leftmost glyph of the + * second visible line in the stem and asserts that it sits at (or very close + * to) the stem container's content-box left edge — i.e. the line is + * left-aligned, not centered. + * + * The PIE side is the only side asserted because the LSY side already renders + * left-aligned wrapped lines. Asserting absolute pixel parity between sides + * would couple the test to the exact stem text used in the live LSY item; + * since the symptom is purely about alignment, an alignment-only assertion is + * sufficient and item-content independent. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-vic-cloze-punct'; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; +// How close to the content-box left the second line must start. A center- +// aligned wrap leaves tens of px of slack on the left; left alignment puts +// the first glyph within a few px of the content-box edge. +const ALIGNMENT_TOLERANCE_PX = 6; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +/** + * Replaces the leading text of the PIE template

so the sentence is long + * enough to force a wrap at the constrained viewport, while preserving the + * cloze marker and trailing period that follow it. We do not touch the LSY + * side because the symptom is PIE-side and the LSY content already wraps in + * its own way at narrow widths. + */ +async function lengthenPieStemToForceWrap(page: import('@playwright/test').Page) { + await page.evaluate(() => { + const p = document.querySelector('#pie-container .pie-template-line p') as HTMLElement | null; + if (!p) return; + const long = + 'When the screen is narrow enough that the prompt does not fit on a single line, the wrapped second line should remain left aligned and not snap back to the center of the container — please pick the right word for the missing slot:'; + // Replace the original "Choose the right word:" text node with the long + // prompt; preserve the trailing nodes (cloze + " .") untouched. + const firstChild = p.firstChild; + if (firstChild && firstChild.nodeType === Node.TEXT_NODE) { + firstChild.nodeValue = `${long} `; + } + }); +} + +/** + * Returns the line-by-line first-glyph x positions inside the stem element. + * Uses Range.getClientRects() over each text node so we get one rect per + * visual line, then groups rects by their integer top to identify lines. + * For each line we record both the leftmost and rightmost glyph edge so the + * caller can reason about indent vs. leftover space. + */ +function measureLineLeftsFn() { + return (selector: string) => { + const stem = document.querySelector(selector) as HTMLElement | null; + if (!stem) return null; + const stemRect = stem.getBoundingClientRect(); + const stemStyles = getComputedStyle(stem); + const padLeft = parseFloat(stemStyles.paddingLeft) || 0; + const padRight = parseFloat(stemStyles.paddingRight) || 0; + const contentLeft = stemRect.left + padLeft; + const contentRight = stemRect.right - padRight; + const contentWidth = contentRight - contentLeft; + + const lineLefts = new Map(); + const lineRights = new Map(); + const walker = document.createTreeWalker(stem, NodeFilter.SHOW_TEXT); + let node = walker.nextNode() as Text | null; + while (node) { + const range = document.createRange(); + range.selectNodeContents(node); + for (const r of Array.from(range.getClientRects())) { + if (r.width === 0 && r.height === 0) continue; + const key = Math.round(r.top); + const prevL = lineLefts.get(key); + if (prevL === undefined || r.left < prevL) lineLefts.set(key, r.left); + const prevR = lineRights.get(key); + if (prevR === undefined || r.right > prevR) lineRights.set(key, r.right); + } + node = walker.nextNode() as Text | null; + } + const tops = Array.from(lineLefts.keys()).sort((a, b) => a - b); + return { + contentLeft, + contentWidth, + lines: tops.map((top) => { + const left = lineLefts.get(top) as number; + const right = lineRights.get(top) as number; + return { + top, + left, + right, + width: right - left, + deltaFromContentLeft: left - contentLeft, + // How much horizontal space is leftover on the line; this is what + // a center alignment would split between left and right margins. + leftover: contentWidth - (right - left), + }; + }), + }; + }; +} + +test.describe('CONTOOL-2573 — sel-vic wrapped-line alignment parity', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('PIE: when stem text wraps, the second line starts at the left edge (not centered)', async ({ + page, + }) => { + // Constrain viewport so the lengthened stem must wrap. + await page.setViewportSize({ width: 760, height: 900 }); + await openParityRoute(page); + await lengthenPieStemToForceWrap(page); + // Allow re-layout after text mutation. + await page.waitForTimeout(150); + + const measured = await page.evaluate(measureLineLeftsFn(), '#pie-container .pie-template-line'); + expect(measured, 'PIE stem not measurable').not.toBeNull(); + const m = measured as NonNullable; + expect( + m.lines.length, + `PIE stem must wrap to at least 2 lines (got ${m.lines.length}); test setup did not produce a wrap. measured=${JSON.stringify(m)}` + ).toBeGreaterThanOrEqual(2); + + // Pick the wrapped line with the most leftover space — that's where a + // center alignment is most visible. A line whose width nearly fills the + // stem has almost no leftover and won't reveal the symptom regardless of + // the alignment rule. + const wrappedLines = m.lines.slice(1); + const symptomLine = wrappedLines.reduce((a, b) => (a.leftover >= b.leftover ? a : b)); + // For a left-aligned line, the indent should be small regardless of how + // much leftover space the line has. For a center-aligned line, the indent + // is approximately leftover/2 — we use that as the "centered" predicate. + const expectedCenteredIndent = symptomLine.leftover / 2; + expect( + symptomLine.deltaFromContentLeft, + `Wrapped line at top=${symptomLine.top} has indent=${symptomLine.deltaFromContentLeft}px with leftover=${symptomLine.leftover}px (centered would be ~${expectedCenteredIndent}px). Expected left-aligned (≤ ${ALIGNMENT_TOLERANCE_PX}px). All lines: ${JSON.stringify(m.lines)}` + ).toBeLessThanOrEqual(ALIGNMENT_TOLERANCE_PX); + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2574-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2574-parity.spec.ts new file mode 100644 index 00000000..6ef9ac0d --- /dev/null +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-issue-CONTOOL-2574-parity.spec.ts @@ -0,0 +1,169 @@ +/** + * Live parity test for CONTOOL-2574: + * https://illuminate.atlassian.net/browse/CONTOOL-2574 + * + * Reported by content: on the sel_vic and sr_vic variants, when screen real + * estate is limited and the cloze marker would wrap to a new line, the + * preceding word should wrap with it so the cloze is never the first content + * on a wrapped line. PIE breaks before the cloze and leaves it dangling alone + * at the start of a new line; LSY pulls the preceding word along because its + * cloze.js emits literal ` ` characters around the cloze span: + * + * `  ` + * + * The reproduction reuses the variant-sel-vic-cloze-punct sample (already + * registered for CONTOOL-2572/2573). To make the wrap-around-cloze symptom + * deterministic, the test resizes the template-line container and inflates + * the cloze marker's width so the only feasible break point is between the + * preceding word and the cloze. With that setup, the buggy code leaves the + * cloze alone on line 2; the fix mirrors LSY by emitting NBSP next to the + * cloze so the preceding word wraps with it. + * + * The test asserts the PIE side only — LSY satisfies the contract by + * construction (the  s are in its rendered HTML), and the symptom is + * about within-PIE layout behaviour, not pixel parity. + */ + +import { expect, test } from '@playwright/test'; + +const DEMO_ID = 'variant-sel-vic-cloze-punct'; +const CREDENTIALS_PRESENT = !!process.env.LEARNOSITY_CONSUMER_KEY; + +async function openParityRoute(page: import('@playwright/test').Page) { + await page.goto(`/mc-populated-blank/parity?demo=${encodeURIComponent(DEMO_ID)}`); + await page.waitForLoadState('domcontentloaded'); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('#pie-container pie-element-player', { timeout: 20_000 }); + await page.waitForSelector('[data-learnosity-ready="true"]', { timeout: 30_000 }); +} + +/** + * Set up the layout so the cloze marker would naturally wrap with no + * preceding word on the same line. + * + * Approach: + * 1. Use the as-rendered template text from the demo sample so we don't + * perturb the very thing the fix produces (overwriting the rendered text + * node would replace any NBSP the fix inserted with whatever character + * we wrote). + * 2. Inflate the cloze marker's width so it cannot share a line with the + * preceding word at the clamp width. + * 3. Clamp the template-line container so the only feasible break is the + * space between the preamble's last word and the cloze. With the buggy + * code, the cloze ends up alone at the start of line 2; the fix puts + * the preceding word with the cloze on line 2 (or makes the whole tail + * stay together so no break opportunity exists at all). + */ +async function setUpClozeAtStartOfWrappedLine(page: import('@playwright/test').Page) { + await page.evaluate(() => { + const line = document.querySelector('#pie-container .pie-template-line') as HTMLElement | null; + if (line) { + line.style.maxWidth = '14em'; + line.style.width = '14em'; + } + const cloze = document.querySelector('#pie-container .pie-blank-slot') as HTMLElement | null; + if (cloze) { + cloze.style.minWidth = '7em'; + cloze.style.width = '7em'; + } + }); +} + +/** + * Measure the cloze marker's line and report whether any non-whitespace glyph + * sits to its left on the same visual line. + * + * Same-line predicate: we use the cloze's INNER content rect (the displayed + * answer / placeholder), not the outer cloze bounding box. The outer box is + * taller than the text height because of the underline border + min-h-1.5em + * padding, so its rect would overlap with the preceding line and falsely + * group glyphs as same-line. The inner content rect tracks the text baseline, + * which is what the eye uses to read "same line". + */ +function measureClozeLineFn() { + return (args: { stemSelector: string; clozeSelector: string }) => { + const stem = document.querySelector(args.stemSelector) as HTMLElement | null; + const cloze = document.querySelector(args.clozeSelector) as HTMLElement | null; + if (!stem || !cloze) return null; + const clozeRect = cloze.getBoundingClientRect(); + const innerEl = + (cloze.querySelector('.pie-blank-value, .cloze-marker-empty') as HTMLElement | null) ?? cloze; + const innerRect = innerEl.getBoundingClientRect(); + + const walker = document.createTreeWalker(stem, NodeFilter.SHOW_TEXT); + const sameLineRects: { left: number; right: number; top: number; text: string }[] = []; + const allLineCenters = new Set(); + let node = walker.nextNode() as Text | null; + while (node) { + if (cloze.contains(node)) { + node = walker.nextNode() as Text | null; + continue; + } + const text = node.nodeValue ?? ''; + for (let i = 0; i < text.length; i += 1) { + const ch = text[i]; + if (/\s/.test(ch)) continue; + const range = document.createRange(); + range.setStart(node, i); + range.setEnd(node, i + 1); + const rect = range.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) continue; + const cy = (rect.top + rect.bottom) / 2; + allLineCenters.add(Math.round(cy)); + const sameLine = cy >= innerRect.top - 1 && cy <= innerRect.bottom + 1; + if (!sameLine) continue; + sameLineRects.push({ left: rect.left, right: rect.right, top: rect.top, text: ch }); + } + node = walker.nextNode() as Text | null; + } + + const leftOfCloze = sameLineRects + .filter((r) => r.right <= clozeRect.left + 0.5) + .sort((a, b) => a.left - b.left); + const rightOfCloze = sameLineRects + .filter((r) => r.left >= clozeRect.right - 0.5) + .sort((a, b) => a.left - b.left); + + return { + clozeLeft: clozeRect.left, + lineCount: allLineCenters.size, + leftOfClozeCount: leftOfCloze.length, + rightOfClozeCount: rightOfCloze.length, + // True when the cloze appears alone at the start of its visual line — + // i.e. there is no text glyph on the same line whose right edge is at + // or before the cloze's left edge. This is precisely the buggy state. + clozeIsLineLeader: leftOfCloze.length === 0, + firstLeftGlyph: leftOfCloze[leftOfCloze.length - 1]?.text ?? null, + }; + }; +} + +test.describe('CONTOOL-2574 — vic cloze wraps with preceding word, never alone', () => { + test.skip(!CREDENTIALS_PRESENT, 'Skipped: LEARNOSITY_CONSUMER_KEY not set'); + + test('PIE sel-vic: when the stem wraps, the cloze line is not led by the cloze itself', async ({ + page, + }) => { + await page.setViewportSize({ width: 760, height: 900 }); + await openParityRoute(page); + await setUpClozeAtStartOfWrappedLine(page); + await page.waitForTimeout(150); + + const measured = await page.evaluate(measureClozeLineFn(), { + stemSelector: '#pie-container .pie-template-line', + clozeSelector: '#pie-container .pie-blank-slot', + }); + expect(measured, 'PIE cloze not measurable').not.toBeNull(); + const m = measured as NonNullable; + + expect( + m.lineCount, + `Test setup error: PIE stem did not wrap (lineCount=${m.lineCount}). measured=${JSON.stringify(m)}` + ).toBeGreaterThanOrEqual(2); + + expect( + m.clozeIsLineLeader, + `PIE cloze starts a wrapped line with no preceding word on the same line. measured=${JSON.stringify(m)}. Expected the preceding word (e.g. "is") to wrap together with the cloze (LSY emits   on either side of the cloze).` + ).toBe(false); + }); +}); diff --git a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-sr-vic-parity.spec.ts b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-sr-vic-parity.spec.ts index 3d33f077..97128776 100644 --- a/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-sr-vic-parity.spec.ts +++ b/apps/learnosity-parity-demo/test/e2e/mc-populated-blank-sr-vic-parity.spec.ts @@ -181,8 +181,9 @@ test.describe('sr-vic live parity — visual', () => { }); test('audio transcript is sr-only (not visibly rendered) on PIE side', async ({ page }) => { - await openSrVicParityRoute(page); - const transcript = page.locator('#pie-container .pie-audio-transcript'); + // PIE-only assertion — use the deliver route to avoid waiting on LSY rendering. + await openSrVicRoute(page); + const transcript = page.locator('[data-testid="role-student"] .pie-audio-transcript'); // sr-vic uses showVisibleTranscript: false — transcript should be sr-only or absent const isVisible = await transcript.isVisible().catch(() => false); if (isVisible) { diff --git a/apps/learnosity-parity-demo/test/e2e/parity-regions.ts b/apps/learnosity-parity-demo/test/e2e/parity-regions.ts index 1d1a675c..6a2b4ad6 100644 --- a/apps/learnosity-parity-demo/test/e2e/parity-regions.ts +++ b/apps/learnosity-parity-demo/test/e2e/parity-regions.ts @@ -14,10 +14,22 @@ * choices → .pie-choices-fieldset * audio → .pie-audio-container * - * Learnosity selectors: - * stem → .rli-r1-stem (blank + adjacent tokens) - * choices → .rli-r1-distractors (choice tiles, role="group") - * audio → .rli-r1-instructions (listen button container) + * Learnosity selectors (vary by template family): + * r1 family (sel_r1-*_plusggg variants): + * stem → .rli-r1-stem (blank + adjacent tokens) + * choices → .rli-r1-distractors (choice tiles, role="group") + * audio → .rli-r1-instructions (listen button container) + * s3 family (sel_r1-s3_plusggg) — uses its own rli-s3-* prefix in the + * live (devel) item bundle, not the rli-r1-* prefix shown in the source + * templates. The PIE template is "

{{blank}}

" so the LSY equivalent + * stem is the cloze container itself: + * stem → .rli-s3-cloze-container + * choices → .rli-s3-distractors + * audio → .rli-s3-listen-container + * vic family (sel_vic, sr_vic) — different prefix entirely: + * stem → .rli-vic-stimulus (cloze inside .rli-vic-answer) + * choices → .rli-vic-distractors + * audio → none (sr_vic) or inline (sel_vic — no separate crop) */ export interface RegionOverride { @@ -121,42 +133,47 @@ export const PARITY_REGIONS: Record = { }, }, + // s3 template (sel_r1-s3_plusggg) uses the rli-s3-* prefix in the live + // (devel) item bundle. The PIE template for this demo is "

{{blank}}

" + // so the LSY equivalent is .rli-s3-cloze-container (the cloze itself). 'variant-sel-r1-s3': { stem: { pie: '.pie-template-line', - learnosity: '.rli-r1-stem', + learnosity: '.rli-s3-cloze-container', }, choices: { pie: '.pie-choices-fieldset', - learnosity: '.rli-r1-distractors', + learnosity: '.rli-s3-distractors', }, audio: { pie: '.pie-audio-container', - learnosity: '.rli-r1-instructions', + learnosity: '.rli-s3-listen-container', }, }, - // sel-vic: audio is embedded in the stem area, no separate audio crop needed. + // sel-vic uses the rli-vic-* prefix — different template family. + // The cloze sits inside .rli-vic-stimulus → .rli-vic-answer; the listen + // button is rendered inline so there's no separate audio crop. 'variant-sel-vic': { stem: { pie: '.pie-template-line', - learnosity: '.rli-r1-stem', + learnosity: '.rli-vic-stimulus', }, choices: { pie: '.pie-choices-fieldset', - learnosity: '.rli-r1-distractors', + learnosity: '.rli-vic-distractors', }, }, - // sr-vic: no audio component at all. + // sr-vic shares the rli-vic-* prefix and has no audio component at all. 'variant-sr-vic': { stem: { pie: '.pie-template-line', - learnosity: '.rli-r1-stem', + learnosity: '.rli-vic-stimulus', }, choices: { pie: '.pie-choices-fieldset', - learnosity: '.rli-r1-distractors', + learnosity: '.rli-vic-distractors', }, }, @@ -191,9 +208,10 @@ export const PARITY_REGIONS: Record = { audio: { pie: '.pie-audio-container', learnosity: '.rli-r1-instructions' }, }, + // s3-graphic uses the same s3 template as the text variant — rli-s3-* prefix. 'variant-sel-r1-s3-graphic': { - stem: { pie: '.pie-template-line', learnosity: '.rli-r1-stem' }, - choices: { pie: '.pie-choices-fieldset', learnosity: '.rli-r1-distractors' }, - audio: { pie: '.pie-audio-container', learnosity: '.rli-r1-instructions' }, + stem: { pie: '.pie-template-line', learnosity: '.rli-s3-cloze-container' }, + choices: { pie: '.pie-choices-fieldset', learnosity: '.rli-s3-distractors' }, + audio: { pie: '.pie-audio-container', learnosity: '.rli-s3-listen-container' }, }, }; diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-audio.png index 0898be70..e33d8705 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-choices.png index 3c12fc09..7a9bb0c0 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-audio.png new file mode 100644 index 00000000..bc1051a3 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-choices.png new file mode 100644 index 00000000..333d601c Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-stem.png new file mode 100644 index 00000000..2694672f Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-stem.png index d50860d4..72483105 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-g-stem-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-audio.png index 7f2bb5de..c7889cc4 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-choices.png index 857ca35f..f60af12c 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-audio.png new file mode 100644 index 00000000..fce46715 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-choices.png new file mode 100644 index 00000000..169946ec Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-stem.png new file mode 100644 index 00000000..95705552 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-stem.png index 6a618d6f..a8763e89 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gg-plus-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-audio.png index 0918849b..a3e17bf1 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-choices.png index ec5c615a..8fbf1348 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-audio.png new file mode 100644 index 00000000..ef95f402 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-choices.png new file mode 100644 index 00000000..169946ec Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-stem.png new file mode 100644 index 00000000..fc8abf82 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-stem.png index 398230ba..a09500b2 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-ggplus-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-audio.png index 5c4468c8..68c99f53 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-choices.png index b9a0f8f2..72cf4a25 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-audio.png new file mode 100644 index 00000000..6daa8ec8 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-choices.png new file mode 100644 index 00000000..d2bfdf37 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-stem.png new file mode 100644 index 00000000..50c42406 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-stem.png index 0242787e..4f3090fd 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-gplusggg-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-audio.png index 059e4979..ace19a42 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-choices.png index 38fbd3fb..d1f224c0 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-audio.png new file mode 100644 index 00000000..ace19a42 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-choices.png new file mode 100644 index 00000000..19d1ad7e Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-stem.png new file mode 100644 index 00000000..4b9a0e84 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-stem.png index b019a07f..4b9a0e84 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-plusggg-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-audio.png new file mode 100644 index 00000000..dcc2767c Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-choices.png new file mode 100644 index 00000000..eec2b038 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-audio.png new file mode 100644 index 00000000..3a32682e Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-choices.png new file mode 100644 index 00000000..d9cd682f Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-stem.png new file mode 100644 index 00000000..88f76aad Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-stem.png new file mode 100644 index 00000000..b7b5f40f Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-r1-s3-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-vic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-vic-choices.png new file mode 100644 index 00000000..2dfe589a Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-vic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-vic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-vic-stem.png new file mode 100644 index 00000000..db9f35f2 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sel-vic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sr-vic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sr-vic-choices.png new file mode 100644 index 00000000..afd339e0 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sr-vic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sr-vic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sr-vic-stem.png new file mode 100644 index 00000000..9c353526 Binary files /dev/null and b/apps/learnosity-parity-demo/test/e2e/snapshots/learnosity/learnosity-variant-sr-vic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-audio.png index a7d6ce08..264c44a1 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-choices-selected.png index a1318d78..5b0ce1c3 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices-selected.png index 6588550c..17832775 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices.png index 0a2b231b..eaaefe7c 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem-selected.png index 6132bc68..78d7cf5f 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem.png index b04b12dc..58a38f14 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem-selected.png index 754934e3..65cc5669 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem.png index af30deec..8ba3acaa 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-g-stem-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-audio.png index f29aa00c..02b60b70 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-choices-selected.png index 48e986e1..ece37710 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices-selected.png index 3a6cc8ef..d4f3782c 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices.png index aa334750..858e47b3 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem-selected.png index b53620c0..626f1a5e 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem.png index 14e91adc..d17b08bc 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem-selected.png index 39233441..5d4f4c19 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem.png index b12a2877..3f104945 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gg-plus-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-audio.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-audio.png index a7d6ce08..264c44a1 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-audio.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-audio.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-choices-selected.png index 12336462..b0c3c6f9 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices-selected.png index 3a6cc8ef..d4f3782c 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices.png index aa334750..858e47b3 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem-selected.png index 413f69a5..21aa7954 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem.png index 64b2589c..baae3e21 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem-selected.png index d27c15e8..9045f1aa 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem.png index e5800e05..4b81d7d9 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-ggplus-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-choices-selected.png index 34997640..111dcf44 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices-selected.png index c6bd383c..40f44774 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices.png index 811bcff4..e019d153 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem-selected.png index 12c965f1..967aee26 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem.png index 29faf42e..44876ac1 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-stem-selected.png index c790c388..401649d4 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-gplusggg-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-choices-selected.png index dd7d6222..a0f1db7e 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices-selected.png index ec6e2e2e..e3918099 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices.png index 36a18e56..55136dd3 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem-selected.png index 2a8c6726..89d59c0e 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem.png index b76a09b5..de6f2194 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-stem-selected.png index dc4e5833..5291212b 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-plusggg-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-choices-selected.png index 72652f9c..79ef1f3c 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices-selected.png index 53364b63..a1391a34 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices.png index 4f8aece0..ea128068 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem-selected.png index 712abadd..d9939fbb 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem.png index 0fc52610..c7af0ee3 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-graphic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-stem-selected.png index 42f29e77..535a72ed 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-r1-s3-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices-selected.png index dcb4a202..d803e0b8 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices.png index 205560d4..e817dc5e 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-choices.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem-selected.png index 7e22ea71..22b598d9 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem.png index 6c4e33c5..9ce6f3af 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sel-vic-stem.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-choices-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-choices-selected.png index 3b27f0c4..af7788bc 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-choices-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-choices-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-stem-selected.png b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-stem-selected.png index df69d9f6..81b5d9ce 100644 Binary files a/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-stem-selected.png and b/apps/learnosity-parity-demo/test/e2e/snapshots/pie-variant-sr-vic-stem-selected.png differ diff --git a/apps/learnosity-parity-demo/test/e2e/test-helpers.ts b/apps/learnosity-parity-demo/test/e2e/test-helpers.ts index 1aa2a3ce..992da52e 100644 --- a/apps/learnosity-parity-demo/test/e2e/test-helpers.ts +++ b/apps/learnosity-parity-demo/test/e2e/test-helpers.ts @@ -302,6 +302,27 @@ export function deliveryContainer(page: Page): Locator { return page.locator('.delivery-view .element-container').first(); } +/** + * Disable CSS transitions and animations for the page so that assertions on + * computed styles (e.g. background-color via toHaveCSS) sample the final + * state rather than an intermediate frame. Penetrates shadow roots so it + * applies inside Svelte custom elements as well. + */ +export async function disableTransitions(page: Page) { + const css = `*, *::before, *::after { transition: none !important; animation: none !important; }`; + await page.addStyleTag({ content: css }); + await page.evaluate((styleText) => { + for (const root of document.querySelectorAll('*')) { + const sr = (root as HTMLElement).shadowRoot; + if (sr) { + const style = document.createElement('style'); + style.textContent = styleText; + sr.appendChild(style); + } + } + }, css); +} + /** * Parse session JSON panel; retries for async updates. */ diff --git a/packages/elements-svelte/mc-populated-blank/src/delivery/McPopulatedBlank.svelte b/packages/elements-svelte/mc-populated-blank/src/delivery/McPopulatedBlank.svelte index 6a29efee..16827fcd 100644 --- a/packages/elements-svelte/mc-populated-blank/src/delivery/McPopulatedBlank.svelte +++ b/packages/elements-svelte/mc-populated-blank/src/delivery/McPopulatedBlank.svelte @@ -192,10 +192,24 @@ const templateParts = $derived.by(() => { const t = model?.template || ''; const idx = t.indexOf(BLANK_TOKEN); if (idx < 0) return { before: t, after: '' }; - return { - before: t.slice(0, idx), - after: t.slice(idx + BLANK_TOKEN.length), - }; + let before = t.slice(0, idx); + let after = t.slice(idx + BLANK_TOKEN.length); + // LSY's shared cloze renderer (Renaissance components/src/cloze.js) emits + // literal ` ` on BOTH sides of the cloze span unconditionally: + // `  `. + // Mirror that ONLY for inline_sentence layouts (sel-vic, sr-vic, plain + // sentence). Token-sequence layouts arrange tokens in a grid with explicit + // sibling-margin spacing, so injecting NBSP throws off those tuned gaps; + // they don't suffer the wrap-leader symptom either because each token has + // its own grid placement. This (a) keeps the cloze glued to its neighbors + // so it doesn't dangle at the start of a wrapped line (CONTOOL-2574), and + // (b) matches LSY's cloze→adjacent-text gap when the author wrote no + // whitespace around {{blank}} (CONTOOL-2572: e.g. `…word: {{blank}}.`). + if (layoutProfile === 'inline_sentence') { + before = `${before.replace(/\s+$/, '')} `; + after = ` ${after.replace(/^\s+/, '')}`; + } + return { before, after }; }); function emitSession(updatedSession: any, sourceEl?: HTMLElement | null) { @@ -435,9 +449,8 @@ $effect(() => { {/if} {#if !isAudioOnlyMode} -
- {@html templateParts.before} - {@html templateParts.before} { blankWidth={layout.blankWidth} blankBorderWidth={layout.blankBorderWidth} ariaLabel={uiText.selectedAnswerInSentence} - /> - {@html templateParts.after} -
+ />{@html templateParts.after} {/if} {#if resultText} diff --git a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-base.css b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-base.css index 4851b77c..b996f879 100644 --- a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-base.css +++ b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-base.css @@ -3,9 +3,15 @@ Uses [class*="variant-sel-r1-"] so it covers every r1 variant class. Source: web-ItemBankViewer/learnosity/templates/Renaissance/r1.scss */ -/* r1.scss: Starial font throughout */ +/* r1.scss: Starial font throughout. + font-size 14px mirrors the Learnosity host root (.lrn_widget) which sets the + base for all em-scaled rules in r1.scss (e.g. .rli-r1-content-element { 1.9em }) + and any em-based author content (e.g. ). Without + this base PIE inherits the 16px browser default and renders ~14% larger than + the LSY reference. See CONTOOL-2567. */ [class*="variant-sel-r1-"] { font-family: Starial, Arial, sans-serif; + font-size: 14px; } /* r1.scss: .rli-r1-audio-transcript { font-family:Roboto; font-size:16px } */ @@ -24,13 +30,16 @@ /* r1.scss: .rli-r1-stem { display:flex; flex-direction:row; justify-content:center } r1.scss: .rli-r1-content-element { margin-right:2px; margin-left:2px } — 4px total gap. - column-gap:4px replicates the 2px+2px per-element margin. */ + Apply the per-element margin to both classless tokens and the blank slot + (which is the cloze equivalent) so the gap stacks with whitespace text-nodes + between flex items the same way LSY does. Earlier this used column-gap:4px, + but flex column-gap accumulates with the cloze-side mx-1 and the implicit + whitespace, producing a wider gap than LSY shows. See CONTOOL-2569. */ [class*="variant-sel-r1-"] .pie-template-line { display: flex; flex-direction: row; align-items: baseline; justify-content: center; - column-gap: 4px; } [class*="variant-sel-r1-"] .pie-template-line p { @@ -38,18 +47,44 @@ display: flex; flex-direction: row; align-items: baseline; - gap: 4px; } /* r1.scss: .rli-r1-content-element { min-width:150px; max-width:150px; min-height:150px; - line-height:89px } — every stem token (blank slot and adjacent spans) is a 150px box. - Scoped to token_sequence layout so the audio_blank_only standalone blank (160px) is unaffected. */ -[class*="variant-sel-r1-"].layout-token_sequence .pie-template-line span:not([class]), + line-height:89px; margin-right:2px; margin-left:2px } — wraps every stem token + (blank slot and adjacent inline spans) in LSY, giving each a 150px box with a + 2px per-side margin. Apply to inline tokens in all r1 variants so trailing + tokens in audio_blank_only (gplusggg) match the blank's height the way LSY does. */ +[class*="variant-sel-r1-"] .pie-template-line span:not([class]) { + min-width: 150px; + max-width: 150px; + min-height: 150px; + line-height: 89px; + margin-left: 2px; + margin-right: 2px; +} + +/* In token_sequence layouts the blank itself is also a content-element box (no + surrounding .rli-r1-cloze container at 160px). audio_blank_only / stimulus_image_blank + keep their own 160px min-height set on the blank slot in McPopulatedBlank.svelte to + mirror LSY's .rli-r1-cloze height. The 2px per-side margin matches LSY's + .rli-r1-content-element pattern (overriding the default Tailwind mx-1). */ [class*="variant-sel-r1-"].layout-token_sequence .pie-template-line .pie-blank-slot { min-width: 150px; max-width: 150px; min-height: 150px; line-height: 89px; + margin-left: 2px; + margin-right: 2px; +} + +/* audio_blank_only / stimulus_image_blank keep the 160px wide blank slot, but + still need the 2px per-side margin so the gap to the trailing token mirrors + LSY's .rli-r1-content-element rhythm. Without this, Tailwind's mx-1 (4px) + would dominate and the cloze→token gap diverges from LSY. See CONTOOL-2569. */ +[class*="variant-sel-r1-"].layout-audio_blank_only .pie-template-line .pie-blank-slot, +[class*="variant-sel-r1-"].layout-stimulus_image_blank .pie-template-line .pie-blank-slot { + margin-left: 2px; + margin-right: 2px; } /* r1.scss: .rli-r1-content-element { font-size:1.9em; line-height:89px } on distractors */ @@ -57,3 +92,15 @@ font-size: 1.9em; line-height: 89px; } + +/* r1.scss: .rli-r1-content-element { min-width:150px; max-width:150px; + min-height:150px } wraps every image (stem token + distractor) and forces + the content box to 150×150. LSY's host also rewrites + to 150 server-side. PIE keeps the authored attributes (typically 108×108), + so without an explicit override images render ~28% smaller than LSY. + See CONTOOL-2570. */ +[class*="variant-sel-r1-"] .pie-template-line img, +[class*="variant-sel-r1-"] .pie-choice-label img { + width: 150px; + height: 150px; +} diff --git a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-ggplus.css b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-ggplus.css index 092d8dce..400526a4 100644 --- a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-ggplus.css +++ b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-ggplus.css @@ -10,8 +10,3 @@ --mpb-choice-hover-bg: #f2f2f2; --mpb-choice-selected-bg: #fcfcd3; } - -/* In this variant, the blank appears before trailing tokens; avoid left-margin offset. */ -.mc-populated-blank-root.variant-sel-r1-ggplus.layout-token_sequence .pie-blank-slot { - margin-left: 0; -} diff --git a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-s3.css b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-s3.css index 0ebf7407..91d6e48f 100644 --- a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-s3.css +++ b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-r1-s3.css @@ -17,12 +17,14 @@ --mpb-blank-wide-width: 6.8rem; } -/* s3 overrides base choice-label with font-weight:500 */ +/* sel_r1-s3_plusggg/question.css does not set font-weight on distractors or + cloze content-element, so they inherit 400 from the host root. The earlier + font-weight:500 override caused PIE to render visibly heavier than LSY for + the same author content. See CONTOOL-2571. */ .mc-populated-blank-root.variant-sel-r1-s3 .pie-choice-label, .mc-populated-blank-root.variant-sel-r1-s3 .pie-choice-label :is(span, p, div) { font-size: 1.9em; line-height: 89px; - font-weight: 500; } .mc-populated-blank-root.variant-sel-r1-s3 .pie-blank-value, diff --git a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-vic.css b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-vic.css index 44e590a8..8a6d9ed5 100644 --- a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-vic.css +++ b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sel-vic.css @@ -2,7 +2,10 @@ /* Reference layout: web-ItemBankViewer/learnosity/templates/Renaissance/sel_vic/scss/main.scss + vic.scss */ .mc-populated-blank-root.variant-sel-vic { font-family: Starial, Arial, sans-serif; - font-size: 1.5em; + /* LSY chain: .lrn_widget (14px host) → .sel-vic { 1.5em } = 21px. + PIE collapses both LSY layers into this single variant-root selector, so + the resolved value is set directly. See CONTOOL-2567. */ + font-size: 21px; max-width: 800px; margin-inline: auto; --mpb-inline-grid-column-gap: 1rem; @@ -45,6 +48,26 @@ margin: 0; } +/* LSY .rli-vic-answer has no text-align rule and inherits start; PIE's + inline_sentence layout sets text-align:center on .template-line via a + Svelte-scoped selector with specificity (0,4,0), so we need to match or + exceed that. Pin sel-vic back to start so wrapped lines stay left-aligned. + See CONTOOL-2573. */ +.mc-populated-blank-root.variant-sel-vic.layout-inline_sentence .pie-template-line.template-line { + text-align: start; +} + +/* vic.scss: .rli-vic-cloze { margin-left: 6px; margin-right: 6px } gives the + cloze breathing room from adjacent text or punctuation. The PIE markup uses + Tailwind-style mx-1 in the class list, but mc-populated-blank does not + compile Tailwind, so that class is a no-op. Set the inline margin + explicitly so the cloze→trailing-punctuation gap matches LSY. See + CONTOOL-2572. */ +.mc-populated-blank-root.variant-sel-vic .pie-template-line .pie-blank-slot { + margin-left: 6px; + margin-right: 6px; +} + /* vic.scss: .rli-vic-cloze > .rli-vic-content-element { color: #cc3333 } */ .mc-populated-blank-root.variant-sel-vic .pie-blank-value { color: #cc3333; diff --git a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sr-vic.css b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sr-vic.css index dc56617c..a0b7863f 100644 --- a/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sr-vic.css +++ b/packages/elements-svelte/mc-populated-blank/src/delivery/cqt-css/sr-vic.css @@ -22,6 +22,15 @@ margin: 0; } +/* LSY .rli-vic-answer has no text-align rule and inherits start; PIE's + inline_sentence layout sets text-align:center on .template-line via a + Svelte-scoped selector with specificity (0,4,0), so we need to match or + exceed that. Pin sr-vic back to start so wrapped lines stay left-aligned. + Same root cause as sel-vic. See CONTOOL-2573. */ +.mc-populated-blank-root.variant-sr-vic.layout-inline_sentence .pie-template-line.template-line { + text-align: start; +} + /* vic.scss: .rli-vic-cloze > .rli-vic-content-element { color: #cc3333 } */ .mc-populated-blank-root.variant-sr-vic .pie-blank-value { color: #cc3333;