391 lines
19 KiB
TypeScript
391 lines
19 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
||
import * as path from 'node:path'
|
||
import { describe, expect, it } from 'vitest'
|
||
import { FAVORITE_SLOTS, favoriteCommand, favoriteContext, favoriteViewId } from '../favorites/favoriteSlots'
|
||
import { UNSTAGED_MARKER } from '../uncommitted/uncommittedFiles'
|
||
import { COPY_COMMANDS } from './copyText'
|
||
import { entryContext, GROUP_CONTEXT } from './fileEntries'
|
||
import { rowDecoration } from './rowDecoration'
|
||
|
||
interface MenuItem { command?: string, submenu?: string, when: string, group?: string }
|
||
interface Manifest {
|
||
contributes: {
|
||
commands: { command: string, title: string, icon?: string }[]
|
||
submenus: { id: string, label: string, icon?: string }[]
|
||
menus: Record<string, MenuItem[]>
|
||
views: Record<string, { id: string, name: string, when?: string }[]>
|
||
}
|
||
}
|
||
|
||
const { commands, submenus, menus, views } = (JSON.parse(readFileSync(path.resolve(__dirname, '../../../package.json'), 'utf8')) as Manifest).contributes
|
||
|
||
/** 上下文键的值:没设的是 undefined */
|
||
type Context = Record<string, string | boolean | string[] | undefined>
|
||
|
||
/**
|
||
* 照 VS Code 的规则算 when 子句,够菜单里用的写法:`&&` `||` `!` 括号、`==` `!=`、`=~ /正则/`、`in 列表`、单独一个键(有值算真)。
|
||
* `&&` 比 `||` 先算,和 VS Code 一样。
|
||
*/
|
||
function evaluate(when: string, context: Context): boolean {
|
||
let at = 0
|
||
const skip = (): void => {
|
||
while (when[at] === ' ')
|
||
at++
|
||
}
|
||
const eat = (token: string): boolean => {
|
||
skip()
|
||
if (!when.startsWith(token, at))
|
||
return false
|
||
at += token.length
|
||
return true
|
||
}
|
||
const read = (pattern: RegExp): RegExpExecArray => {
|
||
skip()
|
||
const match = pattern.exec(when.slice(at))
|
||
if (!match)
|
||
throw new Error(`读不懂 when:${when}(在「${when.slice(at)}」)`)
|
||
at += match[0].length
|
||
return match
|
||
}
|
||
const primary = (): boolean => {
|
||
if (eat('(')) {
|
||
const value = or()
|
||
if (!eat(')'))
|
||
throw new Error(`括号没配对:${when}`)
|
||
return value
|
||
}
|
||
const key = read(/^[\w.]+/)[0]
|
||
if (eat('=~')) {
|
||
const [, source, flags] = read(/^\/((?:\\.|[^/\\])*)\/([a-z]*)/)
|
||
return new RegExp(source, flags).test(String(context[key] ?? ''))
|
||
}
|
||
if (eat('=='))
|
||
return String(context[key]) === read(/^[\w.]+/)[0]
|
||
if (eat('!='))
|
||
return String(context[key]) !== read(/^[\w.]+/)[0]
|
||
if (eat('in ')) {
|
||
const list = context[read(/^[\w.]+/)[0]]
|
||
return Array.isArray(list) && list.includes(String(context[key]))
|
||
}
|
||
return Boolean(context[key])
|
||
}
|
||
const unary = (): boolean => eat('!') ? !unary() : primary()
|
||
function and(): boolean {
|
||
let value = unary()
|
||
while (eat('&&')) {
|
||
const right = unary()
|
||
value = value && right
|
||
}
|
||
return value
|
||
}
|
||
function or(): boolean {
|
||
let value = and()
|
||
while (eat('||')) {
|
||
const right = and()
|
||
value = value || right
|
||
}
|
||
return value
|
||
}
|
||
const value = or()
|
||
skip()
|
||
if (at !== when.length)
|
||
throw new Error(`when 没读完:${when}`)
|
||
return value
|
||
}
|
||
|
||
const FILES = 'superpowers.codeReader.files'
|
||
const UNCOMMITTED = 'superpowers.codeReader.uncommitted'
|
||
const FAVORITE = favoriteViewId(7)
|
||
const READER_VIEWS = [FILES, UNCOMMITTED, ...FAVORITE_SLOTS.map(favoriteViewId)]
|
||
const REVIEWING = 'superpowers.codeReader.reviewing'
|
||
|
||
const COPY_MENU = 'superpowers.codeReader.copy'
|
||
const ADD_NOTE = 'superpowers.codeReader.addNote'
|
||
const EDIT_NOTE = 'superpowers.codeReader.editNote'
|
||
const STAGE = 'superpowers.codeReader.stage'
|
||
const ASK = 'superpowers.codeReader.askPath'
|
||
const ADD_FAVORITE = 'superpowers.codeReader.addFavorite'
|
||
|
||
const file = entryContext({ isDirectory: false, isRoot: false })
|
||
const folder = entryContext({ isDirectory: true, isRoot: false })
|
||
const root = entryContext({ isDirectory: true, isRoot: true })
|
||
/** 「未提交的更改」里「更改」组的文件行 */
|
||
const unstaged = entryContext({ isDirectory: false, isRoot: false }, UNSTAGED_MARKER)
|
||
const noted = (context: string): string => rowDecoration(context, 'x', '注释', undefined)!.contextValue
|
||
|
||
const orderOf = (item: MenuItem): number => Number(item.group?.split('@')[1] ?? 0)
|
||
const palette = new Map(menus.commandPalette.map(item => [item.command, item.when]))
|
||
const titleOf = (id: string): { title: string, icon?: string } | undefined => commands.find(item => item.command === id)
|
||
|
||
/** 在 view 这一栏(context 是别的上下文键)的某一行上,某个菜单的某一组(行尾按钮 inline / 右键菜单的 copy、notes…)显示哪些命令或子菜单,照 `@序号` 排 */
|
||
function itemsFor(menu: string, group: string, row: string, view = FILES, context: Context = {}): string[] {
|
||
return menus[menu]
|
||
.filter(item => item.group?.split('@')[0] === group && evaluate(item.when, { isMac: true, ...context, view, viewItem: row }))
|
||
.sort((a, b) => orderOf(a) - orderOf(b))
|
||
.map(item => item.command ?? item.submenu ?? '')
|
||
}
|
||
|
||
/** view 这一栏标题栏上显示哪些按钮,照 `@序号` 排 */
|
||
function titleBar(view: string, context: Context = {}): string[] {
|
||
return menus['view/title']
|
||
.filter(item => evaluate(item.when, { ...context, view }))
|
||
.sort((a, b) => orderOf(a) - orderOf(b))
|
||
.map(item => item.command ?? '')
|
||
}
|
||
|
||
const FILE_COPIES = [
|
||
'superpowers.codeReader.copyFileName',
|
||
'superpowers.codeReader.copyFileStem',
|
||
'superpowers.codeReader.copyRelativePath',
|
||
'superpowers.codeReader.copyAbsolutePath',
|
||
]
|
||
const FOLDER_COPIES = [
|
||
'superpowers.codeReader.copyFolderName',
|
||
'superpowers.codeReader.copyRelativePath',
|
||
'superpowers.codeReader.copyAbsolutePath',
|
||
]
|
||
|
||
describe('菜单的 when 子句', () => {
|
||
it('每一条都读得懂(括号配对、正则写全)', () => {
|
||
for (const item of Object.values(menus).flat())
|
||
expect(() => evaluate(item.when, {})).not.toThrow()
|
||
})
|
||
|
||
it('行菜单只挂在代码阅读的各栏,别的视图(看板、资源管理器)上没有', () => {
|
||
for (const view of ['superpowers.kanban', 'workbench.explorer.fileView'])
|
||
expect(itemsFor('view/item/context', 'inline', file, view)).toEqual([])
|
||
})
|
||
})
|
||
|
||
describe('侧栏行尾按钮', () => {
|
||
it('每一栏都是先「问 AI」再「复制」再注释:没注释的是「添加注释」,有注释的是「编辑注释」;根都没有', () => {
|
||
for (const view of READER_VIEWS) {
|
||
expect(itemsFor('view/item/context', 'inline', file, view)).toEqual([ASK, COPY_MENU, ADD_NOTE])
|
||
expect(itemsFor('view/item/context', 'inline', noted(file), view)).toEqual([ASK, COPY_MENU, EDIT_NOTE])
|
||
expect(itemsFor('view/item/context', 'inline', root, view)).toEqual([])
|
||
}
|
||
})
|
||
|
||
it('目录行在主栏、收藏栏多一个「收藏目录」,排在最后;「未提交的更改」里没有', () => {
|
||
for (const view of [FILES, FAVORITE]) {
|
||
expect(itemsFor('view/item/context', 'inline', folder, view)).toEqual([ASK, COPY_MENU, ADD_NOTE, ADD_FAVORITE])
|
||
expect(itemsFor('view/item/context', 'inline', noted(folder), view)).toEqual([ASK, COPY_MENU, EDIT_NOTE, ADD_FAVORITE])
|
||
}
|
||
expect(itemsFor('view/item/context', 'inline', folder, UNCOMMITTED)).toEqual([ASK, COPY_MENU, ADD_NOTE])
|
||
})
|
||
|
||
it('「未提交的更改」里「更改」组的文件行多一个「暂存」,排在最后;别的行、别的栏都没有', () => {
|
||
expect(itemsFor('view/item/context', 'inline', unstaged, UNCOMMITTED)).toEqual([ASK, COPY_MENU, ADD_NOTE, STAGE])
|
||
expect(itemsFor('view/item/context', 'inline', noted(unstaged), UNCOMMITTED)).toEqual([ASK, COPY_MENU, EDIT_NOTE, STAGE])
|
||
for (const context of [file, folder, root, noted(file), noted(folder)])
|
||
expect(itemsFor('view/item/context', 'inline', context, UNCOMMITTED)).not.toContain(STAGE)
|
||
for (const view of [FILES, FAVORITE])
|
||
expect(itemsFor('view/item/context', 'inline', unstaged, view)).not.toContain(STAGE)
|
||
})
|
||
|
||
it('分组行(「暂存的更改」「更改」)什么按钮、菜单都没有', () => {
|
||
for (const group of ['inline', 'copy', 'fileManager', 'notes', 'star'])
|
||
expect(itemsFor('view/item/context', group, GROUP_CONTEXT, UNCOMMITTED)).toEqual([])
|
||
expect(itemsFor(COPY_MENU, 'copy', GROUP_CONTEXT, UNCOMMITTED)).toEqual([])
|
||
})
|
||
|
||
it('「复制」是带 copy 图标的子菜单(VS Code 画成点开的下拉按钮),注释按钮的图标是 note / edit', () => {
|
||
expect(submenus.find(item => item.id === COPY_MENU)).toEqual({ id: COPY_MENU, label: '复制', icon: '$(copy)' })
|
||
expect(titleOf(ADD_NOTE)?.icon).toBe('$(note)')
|
||
expect(titleOf(EDIT_NOTE)?.icon).toBe('$(edit)')
|
||
})
|
||
|
||
it('「问 AI」文件和文件夹(含未提交、有注释)都有,审查时也不藏,命令面板里没有', () => {
|
||
for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)])
|
||
expect(itemsFor('view/item/context', 'inline', context, FILES, { [REVIEWING]: true })[0]).toBe(ASK)
|
||
expect(titleOf(ASK)).toMatchObject({ title: '问 AI', icon: '$(sparkle)' })
|
||
expect(palette.get(ASK)).toBe('false')
|
||
})
|
||
})
|
||
|
||
describe('复制菜单', () => {
|
||
it.each([COPY_MENU, 'view/item/context'])('%s:每一栏都是文件四项、文件夹没有「不带后缀」,有没有注释都一样', (menu) => {
|
||
for (const view of READER_VIEWS) {
|
||
for (const context of [file, noted(file), unstaged, noted(unstaged)])
|
||
expect(itemsFor(menu, 'copy', context, view)).toEqual(FILE_COPIES)
|
||
for (const context of [folder, noted(folder)])
|
||
expect(itemsFor(menu, 'copy', context, view)).toEqual(FOLDER_COPIES)
|
||
expect(itemsFor(menu, 'copy', root, view)).toEqual([])
|
||
}
|
||
})
|
||
|
||
it('菜单上的字', () => {
|
||
expect(FILE_COPIES.map(id => titleOf(id)?.title)).toEqual(['复制文件名', '复制文件名(不带后缀)', '复制相对路径', '复制绝对路径'])
|
||
expect(titleOf('superpowers.codeReader.copyFolderName')?.title).toBe('复制文件夹名')
|
||
})
|
||
|
||
it('命令都登记了、都有注册,命令面板里藏起来', () => {
|
||
for (const id of [...new Set([...FILE_COPIES, ...FOLDER_COPIES])]) {
|
||
expect(id in COPY_COMMANDS).toBe(true)
|
||
expect(titleOf(id)).toBeDefined()
|
||
expect(palette.get(id)).toBe('false')
|
||
}
|
||
expect(Object.keys(COPY_COMMANDS).sort()).toEqual([...new Set([...FILE_COPIES, ...FOLDER_COPIES])].sort())
|
||
})
|
||
})
|
||
|
||
describe('右键菜单的注释', () => {
|
||
it('每一栏没注释的目录 / 文件只有「添加注释」,有注释的只有「编辑注释」,根没有', () => {
|
||
for (const view of READER_VIEWS) {
|
||
for (const context of [file, folder, unstaged]) {
|
||
expect(itemsFor('view/item/context', 'notes', context, view)).toEqual([ADD_NOTE])
|
||
expect(itemsFor('view/item/context', 'notes', noted(context), view)).toEqual([EDIT_NOTE])
|
||
}
|
||
expect(itemsFor('view/item/context', 'notes', root, view)).toEqual([])
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('右键菜单的「在系统文件管理器中显示」', () => {
|
||
/** 菜单上的字跟着系统走,和 VS Code 自带资源管理器一样 */
|
||
const REVEALS = [
|
||
{ platform: 'isMac', command: 'superpowers.codeReader.revealInFinder', title: '在 Finder 中显示' },
|
||
{ platform: 'isWindows', command: 'superpowers.codeReader.revealInFileExplorer', title: '在文件资源管理器中显示' },
|
||
{ platform: 'isLinux', command: 'superpowers.codeReader.openContainingFolder', title: '打开所在的文件夹' },
|
||
]
|
||
|
||
it('每个系统只出自己那一项:每一栏的文件、文件夹(含未提交、有注释)都有,根没有;审查时也不藏', () => {
|
||
for (const { platform, command } of REVEALS) {
|
||
const system = { isMac: false, [platform]: true, [REVIEWING]: true }
|
||
for (const view of READER_VIEWS) {
|
||
for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)])
|
||
expect(itemsFor('view/item/context', 'fileManager', context, view, system)).toEqual([command])
|
||
expect(itemsFor('view/item/context', 'fileManager', root, view, system)).toEqual([])
|
||
}
|
||
}
|
||
})
|
||
|
||
it('字和 VS Code 自带的一样,只在右键菜单里,不挂行尾按钮,命令面板里藏起来', () => {
|
||
for (const { command, title } of REVEALS) {
|
||
expect(titleOf(command)?.title).toBe(title)
|
||
expect(menus['view/item/context'].filter(item => item.command === command)).toHaveLength(1)
|
||
expect(palette.get(command)).toBe('false')
|
||
}
|
||
})
|
||
|
||
it('右键菜单分组:复制、在系统文件管理器中显示、注释、收藏目录(VS Code 按组名的字母序排组,组和组之间画分隔线)', () => {
|
||
const groups = menus['view/item/context']
|
||
.filter(item => item.group !== undefined && !item.group.startsWith('inline'))
|
||
.map(item => item.group!.split('@')[0])
|
||
expect([...new Set(groups)].sort((a, b) => a.localeCompare(b))).toEqual(['copy', 'fileManager', 'notes', 'star'])
|
||
})
|
||
})
|
||
|
||
describe('主栏的审查工单', () => {
|
||
const PICK = 'superpowers.codeReader.pickReviewIssue'
|
||
const HIDE_TESTS = 'superpowers.codeReader.hideReviewTests'
|
||
const SHOW_TESTS = 'superpowers.codeReader.showReviewTests'
|
||
const SHOW_ALL = 'superpowers.codeReader.showAllFiles'
|
||
const REFRESH = 'superpowers.codeReader.refresh'
|
||
const TESTS_HIDDEN = 'superpowers.codeReader.reviewTestsHidden'
|
||
|
||
it('标题栏:平时是选工单、刷新;审查时多出隐藏 / 显示测试文件(按开关换着显示)和「显示全部」', () => {
|
||
expect(titleBar(FILES)).toEqual([PICK, REFRESH])
|
||
expect(titleBar(FILES, { [REVIEWING]: true })).toEqual([PICK, HIDE_TESTS, SHOW_ALL, REFRESH])
|
||
expect(titleBar(FILES, { [REVIEWING]: true, [TESTS_HIDDEN]: true })).toEqual([PICK, SHOW_TESTS, SHOW_ALL, REFRESH])
|
||
expect(menus['view/title'].filter(item => item.when.includes(FILES)).every(item => item.group?.startsWith('navigation'))).toBe(true)
|
||
})
|
||
|
||
it('审查时主栏行上没有注释、收藏按钮(那些行是工作副本里的,不记注释、不收藏);别的栏照常有,复制照旧', () => {
|
||
const reviewing = { [REVIEWING]: true }
|
||
expect(itemsFor('view/item/context', 'inline', file, FILES, reviewing)).toEqual([ASK, COPY_MENU])
|
||
expect(itemsFor('view/item/context', 'inline', folder, FILES, reviewing)).toEqual([ASK, COPY_MENU])
|
||
for (const group of ['notes', 'star'])
|
||
expect(itemsFor('view/item/context', group, folder, FILES, reviewing)).toEqual([])
|
||
expect(itemsFor('view/item/context', 'copy', file, FILES, reviewing)).toEqual(FILE_COPIES)
|
||
expect(itemsFor('view/item/context', 'inline', folder, FAVORITE, reviewing)).toEqual([ASK, COPY_MENU, ADD_NOTE, ADD_FAVORITE])
|
||
expect(itemsFor('view/item/context', 'inline', file, UNCOMMITTED, reviewing)).toEqual([ASK, COPY_MENU, ADD_NOTE])
|
||
})
|
||
|
||
it('命令面板:「显示全部」、隐藏 / 显示测试文件只在审查时有', () => {
|
||
expect(evaluate(palette.get(SHOW_ALL)!, {})).toBe(false)
|
||
expect(evaluate(palette.get(SHOW_ALL)!, { [REVIEWING]: true })).toBe(true)
|
||
expect(evaluate(palette.get(HIDE_TESTS)!, { [REVIEWING]: true })).toBe(true)
|
||
expect(evaluate(palette.get(HIDE_TESTS)!, {})).toBe(false)
|
||
expect(evaluate(palette.get(SHOW_TESTS)!, { [REVIEWING]: true, [TESTS_HIDDEN]: true })).toBe(true)
|
||
})
|
||
|
||
it('按钮的字和图标', () => {
|
||
expect(titleOf(PICK)).toMatchObject({ title: '选工单', icon: '$(git-pull-request)' })
|
||
expect(titleOf(HIDE_TESTS)).toMatchObject({ title: '隐藏测试文件', icon: '$(beaker-stop)' })
|
||
expect(titleOf(SHOW_TESTS)).toMatchObject({ title: '显示测试文件', icon: '$(beaker)' })
|
||
expect(titleOf(SHOW_ALL)).toMatchObject({ title: '显示全部', icon: '$(close)' })
|
||
})
|
||
})
|
||
|
||
describe('「未提交的更改」一栏', () => {
|
||
const HIDE_TESTS = 'superpowers.codeReader.hideUncommittedTests'
|
||
const SHOW_TESTS = 'superpowers.codeReader.showUncommittedTests'
|
||
const REFRESH = 'superpowers.codeReader.refreshUncommitted'
|
||
const TESTS_HIDDEN = 'superpowers.codeReader.uncommittedTestsHidden'
|
||
|
||
it('是主栏下面常驻的一栏', () => {
|
||
expect(views['superpowers-reader'].slice(0, 2)).toEqual([
|
||
{ id: FILES, name: '代码阅读' },
|
||
{ id: UNCOMMITTED, name: '未提交的更改' },
|
||
])
|
||
})
|
||
|
||
it('标题栏:隐藏 / 显示测试文件(这一栏自己的开关,和审查的分开)、刷新;不跟着审查变', () => {
|
||
expect(titleBar(UNCOMMITTED)).toEqual([HIDE_TESTS, REFRESH])
|
||
expect(titleBar(UNCOMMITTED, { [TESTS_HIDDEN]: true })).toEqual([SHOW_TESTS, REFRESH])
|
||
expect(titleBar(UNCOMMITTED, { [REVIEWING]: true, 'superpowers.codeReader.reviewTestsHidden': true })).toEqual([HIDE_TESTS, REFRESH])
|
||
})
|
||
|
||
it('按钮的字和图标同主栏的,命令面板里藏起来(只对这一栏)', () => {
|
||
expect(titleOf(HIDE_TESTS)).toMatchObject({ title: '隐藏测试文件', icon: '$(beaker-stop)' })
|
||
expect(titleOf(SHOW_TESTS)).toMatchObject({ title: '显示测试文件', icon: '$(beaker)' })
|
||
expect(titleOf(REFRESH)).toMatchObject({ title: '刷新', icon: '$(refresh)' })
|
||
expect(titleOf(STAGE)).toMatchObject({ title: '暂存', icon: '$(add)' })
|
||
for (const id of [HIDE_TESTS, SHOW_TESTS, REFRESH, STAGE])
|
||
expect(palette.get(id)).toBe('false')
|
||
})
|
||
})
|
||
|
||
describe('收藏栏', () => {
|
||
it(`预先声明 ${FAVORITE_SLOTS.length} 个栏位,排在「未提交的更改」后面,各自有收藏占着时才显示`, () => {
|
||
expect(views['superpowers-reader'].slice(2)).toEqual(FAVORITE_SLOTS.map(slot => ({ id: favoriteViewId(slot), name: `收藏 ${slot}`, when: favoriteContext(slot) })))
|
||
})
|
||
|
||
it('每一栏的标题栏:重命名、取消收藏,只对这一栏', () => {
|
||
for (const slot of FAVORITE_SLOTS)
|
||
expect(titleBar(favoriteViewId(slot))).toEqual([favoriteCommand('rename', slot), favoriteCommand('remove', slot)])
|
||
})
|
||
|
||
it('每个栏位的命令都登记了、字和图标一样,命令面板里藏起来', () => {
|
||
for (const slot of FAVORITE_SLOTS) {
|
||
expect(titleOf(favoriteCommand('rename', slot))).toMatchObject({ title: '重命名', icon: '$(edit)' })
|
||
expect(titleOf(favoriteCommand('remove', slot))).toMatchObject({ title: '取消收藏', icon: '$(star-full)' })
|
||
expect(palette.get(favoriteCommand('rename', slot))).toBe('false')
|
||
expect(palette.get(favoriteCommand('remove', slot))).toBe('false')
|
||
}
|
||
})
|
||
|
||
it('「收藏目录」:行尾按钮和右键菜单同一项,只对目录行(不含根、文件),星标图标,命令面板里藏起来', () => {
|
||
for (const view of [FILES, FAVORITE]) {
|
||
expect(itemsFor('view/item/context', 'star', folder, view)).toEqual([ADD_FAVORITE])
|
||
for (const context of [file, root, unstaged])
|
||
expect(itemsFor('view/item/context', 'star', context, view)).toEqual([])
|
||
}
|
||
expect(itemsFor('view/item/context', 'star', folder, UNCOMMITTED)).toEqual([])
|
||
expect(titleOf(ADD_FAVORITE)).toMatchObject({ title: '收藏目录', icon: '$(star-empty)' })
|
||
expect(palette.get(ADD_FAVORITE)).toBe('false')
|
||
})
|
||
})
|
||
|
||
describe('菜单里用到的', () => {
|
||
it('命令都登记了;旧的筛选按钮、上下文键不再出现', () => {
|
||
const ids = new Set(commands.map(item => item.command))
|
||
for (const item of Object.values(menus).flat().filter(item => item.command?.startsWith('superpowers.codeReader.')))
|
||
expect(ids.has(item.command!)).toBe(true)
|
||
expect(ids.has('superpowers.codeReader.showUncommitted')).toBe(false)
|
||
const whens = Object.values(menus).flat().map(item => item.when)
|
||
expect(whens.some(when => /superpowers\.codeReader\.(?:filter|testsHidden)\b/.test(when))).toBe(false)
|
||
})
|
||
})
|