112 lines
5.8 KiB
JavaScript
112 lines
5.8 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 { 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)
|
|
}
|
|
})
|