From 59d716262e621b4943e53e75311c8fca9bcbe528 Mon Sep 17 00:00:00 2001 From: Patrick de Klein Date: Thu, 13 Feb 2025 16:58:51 +0100 Subject: [PATCH] test: unit tests for shared vocabulary qti-choice-interaction --- .storybook/preview.ts | 4 + .../qti-choice-interaction.docs.stories.ts | 93 ++++++++++++++++++- test/setup/customMatchers.js | 19 ++-- test/setup/toBePositionedRelativeTo.ts | 41 ++++++++ test/storybookMatchers.d.ts | 9 ++ test/vitest.d.ts | 5 + tsconfig.json | 2 +- 7 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 test/setup/toBePositionedRelativeTo.ts create mode 100644 test/storybookMatchers.d.ts diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 9b866abd..74f47d49 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -3,10 +3,12 @@ import { setWcStorybookHelpersConfig } from 'wc-storybook-helpers'; import { withActions } from '@storybook/addon-actions/decorator'; import prettier from 'prettier-v2'; /* https://github.com/storybookjs/storybook/issues/8078#issuecomment-2325332120 */ import HTMLParser from 'prettier-v2/parser-html'; /* https://github.com/storybookjs/storybook/issues/8078#issuecomment-2325332120 */ +import { expect } from '@storybook/test'; import customElements from '../custom-elements.json'; import { customViewports } from './custom-viewport-sizes'; import DocumentationTemplate from './DocumentationTemplate.mdx'; +import { toBePositionedRelativeTo } from '../test/setup/toBePositionedRelativeTo'; import type { Preview } from '@storybook/web-components'; @@ -27,6 +29,8 @@ setWcStorybookHelpersConfig({ renderDefaultValues: false }); +expect.extend({ toBePositionedRelativeTo }); + setCustomElementsManifest(customElements); const preview: Preview = { diff --git a/src/lib/qti-components/qti-interaction/qti-choice-interaction/qti-choice-interaction.docs.stories.ts b/src/lib/qti-components/qti-interaction/qti-choice-interaction/qti-choice-interaction.docs.stories.ts index 560f0636..dccb694f 100644 --- a/src/lib/qti-components/qti-interaction/qti-choice-interaction/qti-choice-interaction.docs.stories.ts +++ b/src/lib/qti-components/qti-interaction/qti-choice-interaction/qti-choice-interaction.docs.stories.ts @@ -1,7 +1,8 @@ import { html, TemplateInstance } from 'lit'; import { getWcStorybookHelpers } from 'wc-storybook-helpers'; -import { expect, fireEvent, fn, waitFor, within } from '@storybook/test'; +import { expect, fireEvent, fn, waitFor } from '@storybook/test'; import { getByShadowRole } from 'shadow-dom-testing-library'; +import { findByShadowTitle, getByShadowText, within } from 'shadow-dom-testing-library'; import type { QtiSimpleChoice } from '../qti-simple-choice'; import type { Meta, StoryObj } from '@storybook/web-components'; @@ -50,6 +51,12 @@ export const ChoiceLabelDecimal: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-decimal' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('1')).toBeTruthy(); + expect(canvas.getByShadowText('2')).toBeTruthy(); + expect(canvas.getByShadowText('3')).toBeTruthy(); } }; @@ -57,6 +64,12 @@ export const ChoiceLabelLowerAlpha: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-lower-alpha' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('a')).toBeTruthy(); + expect(canvas.getByShadowText('b')).toBeTruthy(); + expect(canvas.getByShadowText('c')).toBeTruthy(); } }; @@ -64,6 +77,12 @@ export const ChoiceLabelUpperAlpha: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-upper-alpha' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('A')).toBeTruthy(); + expect(canvas.getByShadowText('B')).toBeTruthy(); + expect(canvas.getByShadowText('C')).toBeTruthy(); } }; @@ -82,6 +101,12 @@ export const ChoiceLabelSuffixPeriod: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-suffix-period' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('A.')).toBeTruthy(); + expect(canvas.getByShadowText('B.')).toBeTruthy(); + expect(canvas.getByShadowText('C.')).toBeTruthy(); } }; @@ -89,6 +114,12 @@ export const ChoiceLabelSuffixParenthesis: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-suffix-parenthesis' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('A)')).toBeTruthy(); + expect(canvas.getByShadowText('B)')).toBeTruthy(); + expect(canvas.getByShadowText('C)')).toBeTruthy(); } }; @@ -100,6 +131,12 @@ export const ChoiceLabelSuffixAlphaParenthesis: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-lower-alpha qti-labels-suffix-parenthesis' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('a)')).toBeTruthy(); + expect(canvas.getByShadowText('b)')).toBeTruthy(); + expect(canvas.getByShadowText('c)')).toBeTruthy(); } }; @@ -107,6 +144,12 @@ export const ChoiceLabelSuffixDecimalPeriod: Story = { render: TemplateThreeOptions, args: { class: 'qti-labels-decimal qti-labels-suffix-period' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByShadowText('1.')).toBeTruthy(); + expect(canvas.getByShadowText('2.')).toBeTruthy(); + expect(canvas.getByShadowText('3.')).toBeTruthy(); } }; @@ -118,6 +161,14 @@ export const ChoiceOrientationVertical: Story = { render: TemplateThreeOptions, args: { class: 'qti-orientation-vertical' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const ChoiceA = canvas.getByText('You must stay with your luggage at all times.'); + const ChoiceB = canvas.getByText('Do not let someone else look after your luggage.'); + const ChoiceC = canvas.getByText('Remember your luggage when you leave.'); + expect(ChoiceB).toBePositionedRelativeTo(ChoiceA, 'below'); + expect(ChoiceC).toBePositionedRelativeTo(ChoiceB, 'below'); } }; @@ -125,6 +176,14 @@ export const ChoiceOrientationHorizontal: Story = { render: TemplateThreeOptions, args: { class: 'qti-orientation-horizontal' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const ChoiceA = canvas.getByText('You must stay with your luggage at all times.'); + const ChoiceB = canvas.getByText('Do not let someone else look after your luggage.'); + const ChoiceC = canvas.getByText('Remember your luggage when you leave.'); + expect(ChoiceB).toBePositionedRelativeTo(ChoiceA, 'right'); + expect(ChoiceC).toBePositionedRelativeTo(ChoiceB, 'right'); } }; @@ -147,6 +206,20 @@ export const ChoiceStacking1: Story = { render: args => TemplateSixOptions({ ...args, 'max-choices': '0' }), args: { class: 'qti-choices-stacking-1' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const hydrogenChoice = canvas.getByText('Hydrogen'); + const heliumChoice = canvas.getByText('Helium'); + const carbonChoice = canvas.getByText('Carbon'); + const oxygenChoice = canvas.getByText('Oxygen'); + const nitrogenChoice = canvas.getByText('Nitrogen'); + const chlorineChoice = canvas.getByText('Chlorine'); + expect(heliumChoice).toBePositionedRelativeTo(hydrogenChoice, 'below'); + expect(carbonChoice).toBePositionedRelativeTo(hydrogenChoice, 'below'); + expect(oxygenChoice).toBePositionedRelativeTo(carbonChoice, 'below'); + expect(nitrogenChoice).toBePositionedRelativeTo(oxygenChoice, 'below'); + expect(chlorineChoice).toBePositionedRelativeTo(nitrogenChoice, 'below'); } }; @@ -154,6 +227,20 @@ export const ChoiceStacking2: Story = { render: args => TemplateSixOptions({ ...args, 'max-choices': '0' }), args: { class: 'qti-choices-stacking-2' + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const hydrogenChoice = canvas.getByText('Hydrogen'); + const heliumChoice = canvas.getByText('Helium'); + const carbonChoice = canvas.getByText('Carbon'); + const oxygenChoice = canvas.getByText('Oxygen'); + const nitrogenChoice = canvas.getByText('Nitrogen'); + const chlorineChoice = canvas.getByText('Chlorine'); + expect(heliumChoice).toBePositionedRelativeTo(hydrogenChoice, 'right'); + expect(carbonChoice).toBePositionedRelativeTo(hydrogenChoice, 'below'); + expect(oxygenChoice).toBePositionedRelativeTo(carbonChoice, 'right'); + expect(nitrogenChoice).toBePositionedRelativeTo(oxygenChoice, 'left'); + expect(chlorineChoice).toBePositionedRelativeTo(nitrogenChoice, 'right'); } }; @@ -184,7 +271,6 @@ export const ChoiceStacking5: Story = { export const ChoiceOrientationStackingH3: Story = { render: args => TemplateSixOptions({ ...args, 'max-choices': '0' }), - args: { class: 'qti-choices-stacking-3 qti-orientation-horizontal' } @@ -192,7 +278,6 @@ export const ChoiceOrientationStackingH3: Story = { export const ChoiceOrientationStackingV3: Story = { render: args => TemplateSixOptions({ ...args, 'max-choices': '0' }), - args: { class: 'qti-choices-stacking-3 qti-orientation-vertical' } @@ -200,7 +285,6 @@ export const ChoiceOrientationStackingV3: Story = { export const ChoiceOrientationStackingV2: Story = { render: TemplateThreeOptions, - args: { class: 'qti-choices-stacking-2 qti-orientation-vertical' } @@ -208,7 +292,6 @@ export const ChoiceOrientationStackingV2: Story = { export const ChoiceOrientationStackingH2: Story = { render: TemplateThreeOptions, - args: { class: 'qti-choices-stacking-2 qti-orientation-horizontal' } diff --git a/test/setup/customMatchers.js b/test/setup/customMatchers.js index 70968ca1..324b408a 100644 --- a/test/setup/customMatchers.js +++ b/test/setup/customMatchers.js @@ -1,16 +1,16 @@ import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { expect } from 'vitest'; -expect.extend({ +export const customMatchers = { toEqualXml(received, expected) { const parser = new XMLParser({ ignoreAttributes: false, - trimValues: true, + trimValues: true }); const builder = new XMLBuilder({ ignoreAttributes: false, - format: true, // Prettify the output for readability + format: true // Prettify the output for readability }); const receivedObj = parser.parse(received); @@ -21,7 +21,7 @@ expect.extend({ if (pass) { return { message: () => `expected XML not to be equal`, - pass: true, + pass: true }; } else { // Convert JSON objects back to XML strings @@ -29,10 +29,11 @@ expect.extend({ const expectedXml = builder.build(expectedObj); return { - message: () => - `Expected XML structures to be equal:\n\nReceived:\n${receivedXml}\n\nExpected:\n${expectedXml}`, - pass: false, + message: () => `Expected XML structures to be equal:\n\nReceived:\n${receivedXml}\n\nExpected:\n${expectedXml}`, + pass: false }; } - }, -}); + } +}; + +expect.extend(customMatchers); diff --git a/test/setup/toBePositionedRelativeTo.ts b/test/setup/toBePositionedRelativeTo.ts new file mode 100644 index 00000000..3a57dab1 --- /dev/null +++ b/test/setup/toBePositionedRelativeTo.ts @@ -0,0 +1,41 @@ +export function toBePositionedRelativeTo(received, other, position) { + if (!(received instanceof Element) || !(other instanceof Element)) { + return { + pass: false, + message: () => `Expected both arguments to be DOM elements.` + }; + } + + const rectA = received.getBoundingClientRect(); + const rectB = other.getBoundingClientRect(); + + // Check for overlap (Fail if they overlap) + const overlaps = + rectA.left < rectB.right && rectA.right > rectB.left && rectA.top < rectB.bottom && rectA.bottom > rectB.top; + + if (overlaps) { + return { + pass: false, + message: () => `Expected elements NOT to overlap, but they do.` + }; + } + + const positionChecks = { + left: rectA.right <= rectB.left, + right: rectA.left >= rectB.right, + above: rectA.bottom <= rectB.top, + below: rectA.top >= rectB.bottom + }; + + if (!positionChecks[position]) { + return { + pass: false, + message: () => `Expected element to be "${position}" relative to the other, but it was not.` + }; + } + + return { + pass: true, + message: () => `Element is correctly positioned "${position}" without overlapping.` + }; +} diff --git a/test/storybookMatchers.d.ts b/test/storybookMatchers.d.ts new file mode 100644 index 00000000..508ee908 --- /dev/null +++ b/test/storybookMatchers.d.ts @@ -0,0 +1,9 @@ +import '@storybook/test'; + +declare global { + namespace jest { + interface Matchers { + toBePositionedRelativeTo(other: Element, position: 'left' | 'right' | 'above' | 'below'): R; + } + } +} diff --git a/test/vitest.d.ts b/test/vitest.d.ts index 5627f6fa..d813756d 100644 --- a/test/vitest.d.ts +++ b/test/vitest.d.ts @@ -2,6 +2,11 @@ import 'vitest'; interface CustomMatchers { toEqualXml: (expected: string) => R; + toBePositionedRelativeTo: (received, other, position: 'left' | 'right' | 'above' | 'below') => R; +} + +interface Assertion { + toBePositionedRelativeTo(other: Element, position: 'left' | 'right' | 'above' | 'below'): void; } declare module 'vitest' { diff --git a/tsconfig.json b/tsconfig.json index 3e7b4b98..cf578f94 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - "types": ["vitest/globals","@types/dom-view-transitions","./test/vitest.d.ts"] + "types": ["vitest/globals","@types/dom-view-transitions","./test/vitest.d.ts", "./test/storybookMatchers.d.ts"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }]