Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<p>Choose the right word: {{blank}}.</p>",
"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)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,23 +215,20 @@ 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);

const templateLine = root.locator('.pie-template-line');
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);
});

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <span style="font-size:1.8em"> 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
);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading