Skip to content

Commit

Permalink
feat: enhance item response handling and add support for serving stat…
Browse files Browse the repository at this point in the history
…ic files
  • Loading branch information
Marcelh1983 committed Feb 12, 2025
1 parent a8928ba commit d991073
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 136 deletions.
5 changes: 3 additions & 2 deletions src/lib/exports/computed-item.context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createContext } from '@lit/context';

import type { VariableDeclaration } from './variables';

export type ComputedItemContext = {
identifier: string;
href?: string;
Expand All @@ -9,8 +11,7 @@ export type ComputedItemContext = {
adaptive?: boolean;
timeDependent?: boolean;
title?: string;
correctResponse?: string;
value?: Readonly<string | string[]>;
variables: ReadonlyArray<VariableDeclaration<string | string[] | null>>;
};

export const computedItemContext = createContext<Readonly<ComputedItemContext>>(Symbol('computedItemContext'));
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import '../../../../../.storybook/import-storybook-cem'; // Fake Storybook import
import { html, render } from 'lit';
import { afterEach, beforeEach, describe, test } from 'vitest';
import { composeStory } from '@storybook/preview-api';

import { resolveLoaders } from '../../../../../.storybook/custom-story-loader';
import meta, {
Default as DefaultStory,
NoCorrectResponse as NoCorrectResponseStory,
MultipleResponse as MultipleResponseStory,
SelectPoint as SelectPointStory,
SelectPointMultipleNoAreaMapping as SelectPointMultipleNoAreaMappingStory,
GraphicOrder as GraphicOrderStory
} from './item-show-correct-response.stories';

import '../../../qti-components';
import './..';
import '../../../../item.css';

// Compose all stories
const defaultStory = composeStory(DefaultStory, meta);
const noCorrectResponseStory = composeStory(NoCorrectResponseStory, meta);
const multipleResponseStory = composeStory(MultipleResponseStory, meta);
const selectPointStory = composeStory(SelectPointStory, meta);
const selectPointMultipleNoAreaMappingStory = composeStory(SelectPointMultipleNoAreaMappingStory, meta);
const graphicOrderStory = composeStory(GraphicOrderStory, meta);

// Helper function to resolve loaders and render story
async function setupStory(story, canvasElement) {
const loaded = await resolveLoaders(story.loaders, story.args);
const args = { ...meta.args, ...(story.args || {}) };

// Ensure `item-url` is correctly prefixed with `/public/assets`
if (args['item-url']) {
args['item-url'] = `${window.location.origin}/public/assets${args['item-url']}`;
}

render(story.render!(args as any, { loaded, argTypes: story.argTypes || {} } as any), canvasElement);
}

describe.sequential('ItemShowCorrectResponse Suite', () => {
let canvasElement: HTMLElement;

beforeEach(() => {
canvasElement = document.createElement('div');
document.body.appendChild(canvasElement);
});

afterEach(() => {
if (canvasElement) {
canvasElement.remove();
canvasElement = null;
}
});

test('show correct response - Default', async () => {
await setupStory(DefaultStory, canvasElement);
await defaultStory.play({ canvasElement });
});

test('show correct response - NoCorrectResponse', async () => {
await setupStory(NoCorrectResponseStory, canvasElement);
await noCorrectResponseStory.play({ canvasElement });
});

test('show correct response - MultipleResponse', async () => {
await setupStory(MultipleResponseStory, canvasElement);
await multipleResponseStory.play({ canvasElement });
});

test('show correct response - SelectPoint', async () => {
await setupStory(SelectPointStory, canvasElement);
await selectPointStory.play({ canvasElement });
});

test('show correct response - SelectPointMultipleNoAreaMapping', async () => {
await setupStory(SelectPointMultipleNoAreaMappingStory, canvasElement);
await selectPointMultipleNoAreaMappingStory.play({ canvasElement });
});

test('show correct response - GraphicOrder', async () => {
await setupStory(GraphicOrderStory, canvasElement);
await graphicOrderStory.play({ canvasElement });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const meta: Meta<typeof ItemContainer & { 'item-url': string }> = {
export default meta;

export const Default: Story = {
render: args =>
html` <qti-item>
render: args => {
return html`<qti-item>
<!-- <div style="display: flex; flex-direction: column; gap: 1rem;"> -->
<item-container style="width: 400px; height: 350px; display: block;" item-url=${args['item-url']}>
<template>
Expand All @@ -49,7 +49,9 @@ export const Default: Story = {
<item-show-correct-response ${spread(args)}></item-show-correct-response>
<!-- </div> -->
</qti-item>`,
</qti-item>`;
},

play: async ({ canvasElement, step }) => {
// wait for qti-simple-choice to be rendered
const canvas = within(canvasElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { consume } from '@lit/context';
import * as styles from './styles';
import { computedItemContext } from '../../../exports/computed-item.context';

import type { ResponseVariable } from '../../../exports/variables';
import type { ComputedItemContext } from '../../../exports/computed-item.context';

@customElement('item-show-correct-response')
Expand All @@ -27,8 +28,13 @@ export class ItemShowCorrectResponse extends LitElement {
@property({ type: String }) hideCorrectText = 'Hide correct response';
@property({ type: String }) noCorrectResponseText = 'No correct response specified';

private _hasCorrectResponse = false; // correct response is removed on certain point

updated() {
this.disabled = !this.computedContext?.correctResponse; // Disable when no correct response
if (!this._hasCorrectResponse) {
this._hasCorrectResponse = this.computedContext?.variables?.some(v => (v as ResponseVariable)?.correctResponse);
}
this.disabled = !this._hasCorrectResponse;
}

private _toggleState() {
Expand Down
69 changes: 67 additions & 2 deletions src/lib/qti-item/core/components/print-item-variables.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,83 @@
import { consume } from '@lit/context';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { customElement, property } from 'lit/decorators.js';

import { computedItemContext } from '../../../exports/computed-item.context';

import type { ResponseVariable } from '../../../exports/variables';
import type { ComputedItemContext } from '../../../exports/computed-item.context';

@customElement('print-item-variables')
export class PrintItemVariables extends LitElement {
@consume({ context: computedItemContext, subscribe: true })
public computedContext?: ComputedItemContext;

@property({ type: String, reflect: true })
public mode: 'summed' | 'complete' = 'summed';

private _previousActiveItem?: ComputedItemContext & { correctResponse: string; response: string } = null; // Store previous active item reference
render() {
return html` <small><pre>${JSON.stringify(this.computedContext, null, 2)}</pre></small> `;
if (this.computedContext) {
const responseVariables: ResponseVariable[] = this.computedContext.variables?.filter(v => {
if (v.type !== 'response') {
return false;
}
if (v.identifier.toLowerCase().startsWith('response')) {
return true;
}
if ((v as ResponseVariable).correctResponse) {
return true;
}
});

// sort the response variables by the order of the string: identifier
const sortedResponseVariables = responseVariables?.sort((a, b) => a.identifier.localeCompare(b.identifier));
const response =
sortedResponseVariables.length === 0
? ''
: sortedResponseVariables
?.map(v => {
if (Array.isArray(v.value)) {
return v.value.join('&');
}
return v.value;
})
.join('#');
const correctResponseArray = sortedResponseVariables.map(r => {
if (r.mapping && r.mapping.mapEntries.length > 0) {
return r.mapping.mapEntries
.map(m => {
return `${m.mapKey}=${m.mappedValue}pt `;
})
.join('&');
}
if (r.areaMapping && r.areaMapping.areaMapEntries.length > 0) {
return r.areaMapping.areaMapEntries.map(m => {
return `${m.coords} ${m.shape}=${m.mappedValue}`;
});
}
if (r.correctResponse) {
return Array.isArray(r.correctResponse) ? r.correctResponse.join('&') : r.correctResponse;
}
return [];
});
const correctResponse = correctResponseArray.join('&');
const printableItemContext = {
...this.computedContext,
response,
correctResponse:
this._previousActiveItem?.identifier === this.computedContext.identifier &&
this._previousActiveItem?.correctResponse
? this._previousActiveItem.correctResponse
: correctResponse
};
if (this.mode === 'summed') {
delete printableItemContext.variables;
}
this._previousActiveItem = printableItemContext;
return html` <small><pre>${JSON.stringify(printableItemContext, null, 2)}</pre></small> `;
}
return html``;
}
}

Expand Down
67 changes: 11 additions & 56 deletions src/lib/qti-item/core/qti-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { computedItemContext } from '../../exports/computed-item.context';

import type { QtiAssessmentItem } from '../../qti-components';
import type { ItemContext } from '../../exports/item.context';
import type { ResponseVariable, VariableDeclaration } from '../../exports/variables';
import type { VariableDeclaration } from '../../exports/variables';
import type { ComputedItemContext } from '../../exports/computed-item.context';

/**
Expand Down Expand Up @@ -45,13 +45,15 @@ export class QtiItem extends LitElement {
private _handleAssessmentItemConnected(e: CustomEvent<QtiAssessmentItem>) {
this._qtiAssessmentItem = e.detail;
this.computedContext =
this.computedContext?.identifier === e.detail.identifier
? { ...this.computedContext, title: e.detail.title }
: {
identifier: e.detail.identifier,
title: e.detail.title
};
this._updateItemVariablesInTestContext(e.detail.identifier, e.detail.variables || []);
this.computedContext?.identifier === this._qtiAssessmentItem.identifier
? { ...this.computedContext, title: this._qtiAssessmentItem.title }
: ({
identifier: this._qtiAssessmentItem.identifier,
title: this._qtiAssessmentItem.title,
adaptive: this._qtiAssessmentItem.getAttribute('adaptive')?.toLowerCase() === 'true' || false,
variables: this._qtiAssessmentItem.variables
} as ComputedItemContext);
this._updateItemVariablesInTestContext(this._qtiAssessmentItem.identifier, e.detail.variables || []);
}

private _handleTestShowCorrectResponse(e: CustomEvent<boolean>) {
Expand All @@ -71,60 +73,13 @@ export class QtiItem extends LitElement {
const correct = score !== undefined && !isNaN(score) && score > 0;
const incorrect = score !== undefined && !isNaN(score) && score <= 0;
const completed = completionStatus === 'completed';
// || item.category === this.host._configContext?.infoItemCategory || false
const responseVariables: ResponseVariable[] = variables?.filter(v => {
if (v.type !== 'response') {
return false;
}
if (v.identifier.toLowerCase().startsWith('response')) {
return true;
}
if ((v as ResponseVariable).correctResponse) {
return true;
}
});

// sort the response variables by the order of the string: identifier
const sortedResponseVariables = responseVariables?.sort((a, b) => a.identifier.localeCompare(b.identifier));
const response =
sortedResponseVariables.length === 0
? ''
: sortedResponseVariables
?.map(v => {
if (Array.isArray(v.value)) {
return v.value.join('&');
}
return v.value;
})
.join('#');
const correctResponseArray = sortedResponseVariables.map(r => {
if (r.mapping && r.mapping.mapEntries.length > 0) {
return r.mapping.mapEntries
.map(m => {
return `${m.mapKey}=${m.mappedValue}pt `;
})
.join('&');
}
if (r.areaMapping && r.areaMapping.areaMapEntries.length > 0) {
return r.areaMapping.areaMapEntries.map(m => {
return `${m.coords} ${m.shape}=${m.mappedValue}`;
});
}
if (r.correctResponse) {
return Array.isArray(r.correctResponse) ? r.correctResponse.join('&') : r.correctResponse;
}
return [];
});

const correctResponse = correctResponseArray.join('&');
this.computedContext = {
...this.computedContext,
identifier,
correct,
incorrect,
completed,
correctResponse: correctResponse ? correctResponse : this.computedContext?.correctResponse || '',
value: response
variables
};
}

Expand Down
Loading

0 comments on commit d991073

Please sign in to comment.