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 { renderToStaticMarkup } = require('react-dom/server') const source = fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/WorkspaceGuidePanel.tsx'), 'utf8') const output = {} let videos = [] vm.runInNewContext(ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText, { exports: output, require: name => name === '../../i18n' ? { t: key => key, useLocaleRender() {} } : name === 'react' ? { ...React, useId: () => 'guide' } : require(name), URL, window: { learningVideo: { open: async url => { videos.push(url) } } } }) function elements(root, predicate) { if (!React.isValidElement(root)) return [] return [...(predicate(root) ? [root] : []), ...React.Children.toArray(root.props.children).flatMap(child => elements(child, predicate))] } function setup(overrides = {}) { const toggled = [] const props = { dark: true, workspaceActive: true, minimized: false, resourceMode: 'material', materialItems: [{ title: 'SLAM', description: '센서 토픽과 TF 상태를 점검하고 지도를 생성합니다.', concepts: ['TF 점검', '지도 저장'], videoUrl: '' }], activeMaterialIndex: 0, expandedMaterialIndexes: [0], topics: [], topicDetails: [], overview: '', resources: [], downloadingResourceIds: [], onToggleMaterial: index => toggled.push(index), ...overrides } const tree = output.default(props) const headers = elements(tree, node => node.type === 'button' && node.props['aria-controls']) return { tree, headers, toggled } } for (const dark of [false, true]) { test(`only the material header toggles the card (${dark ? 'dark' : 'light'})`, () => { const { tree, headers, toggled } = setup({ dark }) assert.equal(headers.length, 1) const header = headers[0] assert.equal(header.props.type, 'button') assert.equal(header.props['aria-expanded'], true) assert.equal(header.props.disabled, false) assert.doesNotMatch(header.props.className, /hover:bg/) assert.match(header.props.className, /focus-visible:ring-2/) const card = elements(tree, node => node.type === 'section' && React.Children.toArray(node.props.children).some(child => child.props?.['aria-controls'] === header.props['aria-controls']))[0] assert.ok(card) assert.equal(card.props.onClick, undefined) assert.equal(card.props.onKeyDown, undefined) assert.equal(card.props.role, undefined) assert.equal(card.props.tabIndex, undefined) header.props.onClick() assert.deepEqual(toggled, [0]) }) test(`description is selectable outside the interactive header (${dark ? 'dark' : 'light'})`, () => { const { tree, headers, toggled } = setup({ dark }) const contentId = headers[0].props['aria-controls'] const body = elements(tree, node => node.props.id === contentId)[0] assert.match(body.props.className, /select-text/) assert.equal(body.props.onClick, undefined) assert.equal(elements(headers[0], node => node.props.id === contentId).length, 0) assert.ok(renderToStaticMarkup(body).includes('센서 토픽과 TF 상태')) assert.equal(elements(body, node => Boolean(node.props.onClick || node.props.onKeyDown)).length, 0) assert.deepEqual(toggled, []) }) test(`header padding and dimensions stay identical when toggled (${dark ? 'dark' : 'light'})`, () => { const expanded = setup({ dark, expandedMaterialIndexes: [0] }) const collapsed = setup({ dark, expandedMaterialIndexes: [] }) for (const state of [expanded, collapsed]) { const header = state.headers[0] const card = elements(state.tree, node => node.type === 'section' && React.Children.toArray(node.props.children).some(child => child.props?.['aria-controls'] === header.props['aria-controls']))[0] assert.match(card.props.className, /\bpx-3 pt-3\b/) assert.doesNotMatch(card.props.className, /px-2\.5|pt-2\.5/) assert.match(header.props.className, /\bflex h-6 w-full\b/) } assert.equal(expanded.headers[0].props.className, collapsed.headers[0].props.className) }) } test('future material remains disabled, while previous/current steps keep native keyboard buttons', () => { const items = ['이전', '현재', '다음'].map(title => ({ title, description: '', videoUrl: '' })) const { headers } = setup({ materialItems: items, activeMaterialIndex: 1, expandedMaterialIndexes: [1] }) assert.deepEqual(headers.map(header => header.props.disabled), [false, false, true]) assert.equal(headers[2].props.onClick, undefined) for (const header of headers) { assert.equal(header.props.type, 'button') assert.equal(header.props.onKeyDown, undefined) } assert.ok(setup({ materialItems: items, allowFutureMaterialSteps: true }).headers.every(header => !header.props.disabled)) }) test('video playback is not nested in the collapse button and does not toggle the card', async () => { videos = [] const { tree, headers, toggled } = setup({ materialItems: [{ title: 'SLAM', description: '설명', videoUrl: 'https://www.youtube.com/watch?v=test123' }] }) const video = elements(tree, node => node.type === 'button' && node.props.className?.includes('aspect-video'))[0] assert.ok(video) assert.equal(elements(headers[0], node => node === video).length, 0) video.props.onClick({ stopPropagation() {} }) await Promise.resolve() assert.deepEqual(videos, ['https://www.youtube.com/embed/test123']) assert.deepEqual(toggled, []) for (const button of elements(tree, node => node.type === 'button')) { assert.equal(React.Children.toArray(button.props.children).flatMap(child => elements(child, node => node.type === 'button')).length, 0) } })