65 lines
3.6 KiB
JavaScript
65 lines
3.6 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const ts = require('typescript');
|
|
const React = require('react');
|
|
|
|
const read = path => fs.readFileSync(require.resolve(`../src/${path}`), 'utf8');
|
|
function compile(source, globals = {}) {
|
|
const exports = {};
|
|
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText;
|
|
vm.runInNewContext(code, { exports, require, ...globals });
|
|
return exports;
|
|
}
|
|
function elements(root, predicate) {
|
|
if (!React.isValidElement(root)) return [];
|
|
return [...(predicate(root) ? [root] : []), ...React.Children.toArray(root.props.children).flatMap(child => elements(child, predicate))];
|
|
}
|
|
const source = ts.createSourceFile('SettingsPage.tsx', read('pages/settings/SettingsPage.tsx'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
const page = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === 'SettingsPage');
|
|
const CourseAiSettings = () => null;
|
|
const { SettingsPage } = compile(page.getText(source), { CourseAiSettings, PageHeader: () => null, Bot: () => null });
|
|
const props = {
|
|
session: { accessToken: 'test-token', user: { id: 'teacher' } },
|
|
selectedCourseId: 'course-a',
|
|
courses: [{ id: 'course-a', title: 'A' }, { id: 'course-b', title: 'B' }],
|
|
};
|
|
|
|
test('settings remounts the credential draft when course or session changes', () => {
|
|
const form = input => elements(SettingsPage(input), node => node.type === CourseAiSettings)[0];
|
|
const initial = form(props);
|
|
assert.equal(initial.props.selectedCourseId, 'course-a');
|
|
assert.notEqual(initial.key, form({ ...props, selectedCourseId: 'course-b' }).key);
|
|
assert.notEqual(initial.key, form({ ...props, session: { ...props.session, accessToken: 'new-token' } }).key);
|
|
});
|
|
|
|
test('settings is width-constrained and centered without a duplicate course selector', () => {
|
|
const tree = SettingsPage(props);
|
|
assert.match(tree.props.className, /mx-auto.*w-full.*max-w-\[920px\]/);
|
|
const header = React.Children.toArray(tree.props.children)[0];
|
|
assert.equal(header.props.action, undefined);
|
|
assert.equal(elements(tree, node => node.type === 'select').length, 0);
|
|
assert.equal(elements(SettingsPage({ ...props, selectedCourseId: '', courses: [] }), node => node.type === CourseAiSettings).length, 0);
|
|
});
|
|
|
|
test('curriculum no longer loads AI settings or offers an agent tab', () => {
|
|
const curriculum = read('pages/curriculum/CurriculumPage.tsx');
|
|
assert.doesNotMatch(curriculum, /getCourseAiSetting|updateCourseAiSetting|handleSaveAiSetting|setActiveTab\('ai'\)/);
|
|
assert.match(read('App.tsx'), /<SettingsPage session=\{session\} selectedCourseId=\{selectedCourseId\} courses=\{courses\} \/>/);
|
|
assert.match(read('App.tsx'), /<InstructorLayout[\s\S]*?onSelectCourse=\{handleSelectCourse\}/);
|
|
});
|
|
|
|
test('analytics is hidden from the menu without deleting its existing route', () => {
|
|
const routes = compile(read('routes.ts').replace('import.meta.env.BASE_URL', JSON.stringify('/lms-adm/')));
|
|
assert.equal(routes.navItems.some(item => item.id === 'analytics'), false);
|
|
assert.equal(routes.navItems.some(item => item.id === 'settings'), true);
|
|
assert.equal(routes.isAppRoute('analytics'), true);
|
|
assert.equal(routes.getRouteFromPathname('/lms-adm/analytics'), 'analytics');
|
|
});
|
|
|
|
test('settings has a general-purpose noun phrase rather than an AI-only sentence', () => {
|
|
const { pageMeta } = compile(read('pageMeta.ts'));
|
|
assert.equal(pageMeta.settings.description, '강좌 및 서비스 환경 설정');
|
|
});
|