Files
Genarrative/src/index.test.ts
T
k88936 498b905bd8
Project CI / Repository checks (push) Successful in 1m21s
Project CI / Frontend tests (push) Successful in 3m9s
Project CI / Backend tests (push) Successful in 4m2s
Project CI / Native shell tests (push) Successful in 15m48s
优化素材库多选操作 (#114)
<video src="attachments/2377fc8b-50e4-4919-ba41-5027b957709f" title="2026-07-27 16-15-53.mp4" controls></video>

- 实现shift ctrl的范围选扩选
- 实现多选下载(为一个zip)
- 原本有搜索和文件夹折叠展开功能, 我的处理是: 只作为方便查找的用途. 最终操作会有"目前一些(比如待删除)元素被隐藏了,请再确认"的提示.

result:
```shell
root@k88936-t14p: ~/Downloads# unzip 未命名画布-选中素材-20260806-195631.zip
Archive:  未命名画布-选中素材-20260806-195631.zip
   creating: 未命名画布-选中素材/
   creating: 未命名画布-选中素材/images/
   creating: 未命名画布-选中素材/media/
 extracting: 未命名画布-选中素材/media/001-角色动作(原始视频).mp4
   creating: 未命名画布-选中素材/sequences/
   creating: 未命名画布-选中素材/sequences/002-角色动作/
   creating: 未命名画布-选中素材/sequences/002-角色动作/frames/
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-01.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-02.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-03.png
...
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-31.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/frames/frame-32.png
 extracting: 未命名画布-选中素材/sequences/002-角色动作/metadata.json
 extracting: 未命名画布-选中素材/sequences/002-角色动作/skeleton.json
 extracting: 未命名画布-选中素材/sequences/002-角色动作/README.md
 extracting: 未命名画布-选中素材/sequences/002-角色动作/manifest.txt
 extracting: 未命名画布-选中素材/metadata.json
 extracting: 未命名画布-选中素材/manifest.txt
 ```

~~动作的导出存在很大的问题, 在另一个pr #117 解决~~

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/114
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-08 11:11:25 +08:00

437 lines
17 KiB
TypeScript

import fs from 'node:fs';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
function readIndexCss() {
return fs.readFileSync(path.resolve(process.cwd(), 'src/index.css'), 'utf8');
}
function readPlatformThemeCss() {
return fs.readFileSync(
path.resolve(process.cwd(), 'packages/shared/src/theme.css'),
'utf8',
);
}
function getCssBlock(source: string, selector: string) {
const selectorIndex = source.indexOf(selector);
expect(
selectorIndex,
`${selector} should exist in src/index.css`,
).toBeGreaterThanOrEqual(0);
const openBraceIndex = source.indexOf('{', selectorIndex);
expect(
openBraceIndex,
`${selector} should open a CSS block`,
).toBeGreaterThanOrEqual(0);
let depth = 0;
for (let index = openBraceIndex; index < source.length; index += 1) {
const char = source[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(openBraceIndex + 1, index);
}
}
}
throw new Error(`${selector} block is not closed`);
}
describe('index stylesheet unread dots', () => {
it('keeps creation home featured assets in a sequential responsive masonry layout', () => {
const css = readIndexCss();
const waterfallBlock = getCssBlock(
css,
'.creation-landing__asset-waterfall',
);
const masonryBlock = getCssBlock(
css,
'.creation-landing__asset-waterfall--masonry',
);
const masonryCardBlock = getCssBlock(
css,
'.creation-landing__asset-waterfall--masonry > .creation-landing__asset-card',
);
const cardBlock = getCssBlock(css, '\n.creation-landing__asset-card {');
const tabletQueryIndex = css.indexOf('@media (max-width: 900px)');
const mobileQueryIndex = css.indexOf('@media (max-width: 560px)');
expect(tabletQueryIndex).toBeGreaterThanOrEqual(0);
expect(mobileQueryIndex).toBeGreaterThanOrEqual(0);
const tabletWaterfallBlock = getCssBlock(
css.slice(tabletQueryIndex),
'.creation-landing__asset-waterfall',
);
const mobileWaterfallBlock = getCssBlock(
css.slice(mobileQueryIndex),
'.creation-landing__asset-waterfall',
);
expect(waterfallBlock).toContain('display: grid;');
expect(waterfallBlock).toContain(
'grid-template-columns: repeat(3, minmax(0, 1fr));',
);
expect(waterfallBlock).toContain('grid-auto-flow: row;');
expect(waterfallBlock).toContain('gap: 0.92rem;');
expect(waterfallBlock).toContain('align-items: start;');
expect(waterfallBlock).not.toContain('column-count');
expect(masonryBlock).toContain('position: relative;');
expect(masonryBlock).toContain('display: block;');
expect(masonryCardBlock).toContain('position: absolute;');
expect(masonryCardBlock).toContain('box-sizing: border-box;');
expect(cardBlock).toContain('display: block;');
expect(cardBlock).toContain('margin: 0;');
expect(cardBlock).toContain('align-self: start;');
expect(cardBlock).not.toContain('break-inside');
expect(tabletWaterfallBlock).toContain(
'grid-template-columns: repeat(2, minmax(0, 1fr));',
);
expect(tabletWaterfallBlock).not.toContain('column-count');
expect(mobileWaterfallBlock).toContain('grid-template-columns: 1fr;');
expect(mobileWaterfallBlock).not.toContain('column-count');
});
it('keeps icon generator decoration from overriding the shared generating animation', () => {
const css = readIndexCss();
const generatingBlock = getCssBlock(
css,
'.image-canvas-editor__generation-frame--generating',
);
const generatingScanBlock = getCssBlock(
css,
'.image-canvas-editor__generation-frame--generating::before',
);
const iconBlock = getCssBlock(
css,
'.image-canvas-editor__generation-frame--icon:not(\n .image-canvas-editor__generation-frame--generating\n )',
);
const iconDecorationBlock = getCssBlock(
css,
'.image-canvas-editor__generation-frame--icon:not(\n .image-canvas-editor__generation-frame--generating\n )::before,\n.image-canvas-editor__generation-frame--icon:not(\n .image-canvas-editor__generation-frame--generating\n )::after',
);
expect(generatingBlock).toContain('cursor: progress;');
expect(generatingScanBlock).toContain(
'animation: image-canvas-generation-scan',
);
expect(iconBlock).toContain('background: linear-gradient(');
expect(iconDecorationBlock).toContain('width: 42%;');
expect(css).not.toContain(
'.image-canvas-editor__generation-frame--icon::before,\n.image-canvas-editor__generation-frame--icon::after',
);
});
it('keeps image canvas loading shimmer clipped inside media frames', () => {
const css = readIndexCss();
const loadingBlock = getCssBlock(
css,
'.project-gallery__canvas-cover-loading,\n.image-canvas-editor__layer-image-loading',
);
const shimmerBlock = getCssBlock(
css,
'.project-gallery__canvas-cover-loading::before,\n.image-canvas-editor__layer-image-loading::before',
);
const keyframesBlock = getCssBlock(
css,
'@keyframes image-canvas-generation-scan',
);
expect(loadingBlock).toContain('overflow: hidden;');
expect(loadingBlock).toContain('background: rgba(226, 232, 240, 0.72);');
expect(loadingBlock).not.toContain(
'animation: image-canvas-generation-scan',
);
expect(shimmerBlock).toContain('left: -64%;');
expect(shimmerBlock).toContain('width: 64%;');
expect(shimmerBlock).toContain('animation: image-canvas-generation-scan');
expect(shimmerBlock).toContain('will-change: transform;');
expect(keyframesBlock).toContain('translateX(-100%)');
expect(keyframesBlock).toContain('translateX(260%)');
});
it('keeps creation home primary rail styling separate from active tab styling', () => {
const css = readIndexCss();
expect(css).not.toContain(
'.platform-desktop-shell--workbench .platform-desktop-rail__button--primary .platform-desktop-rail__icon-shell,\n.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__icon-shell',
);
expect(css).not.toContain(
'.platform-desktop-shell--workbench .platform-desktop-rail__button--primary .platform-desktop-rail__label,\n.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__label',
);
expect(css).toContain(
'.platform-desktop-rail__button--active .platform-desktop-rail__icon-shell',
);
expect(css).toContain(
'.platform-desktop-rail__button--active .platform-desktop-rail__label',
);
});
it('hides the outer page scrollbar without changing inner scroll helpers', () => {
const css = readIndexCss();
const rootBlock = getCssBlock(css, 'html,\nbody,\n#root');
expect(rootBlock).toContain('overflow-x: hidden;');
expect(rootBlock).toContain('-ms-overflow-style: none;');
expect(rootBlock).toContain('scrollbar-width: none;');
expect(css).toContain(
'html,\nbody,\n#root,\n.platform-viewport-shell,\n.platform-tab-panel,\n.platform-page-stage,\n.unified-creation-page,\n.platform-work-detail__scroll',
);
const webkitRootBlock = getCssBlock(
css,
'html::-webkit-scrollbar,\nbody::-webkit-scrollbar,\n#root::-webkit-scrollbar',
);
expect(webkitRootBlock).toContain('display: none;');
expect(webkitRootBlock).toContain('width: 0;');
expect(webkitRootBlock).toContain('height: 0;');
expect(css).toContain('.platform-viewport-shell::-webkit-scrollbar');
expect(css).toContain('.platform-tab-panel::-webkit-scrollbar');
expect(css).toContain('.platform-page-stage::-webkit-scrollbar');
expect(css).toContain('.unified-creation-page::-webkit-scrollbar');
expect(css).toContain('.platform-work-detail__scroll::-webkit-scrollbar');
expect(css).toContain('.creation-home-stage--landing');
expect(css).toContain('scrollbar-width: thin;');
expect(css).toContain(
'.creation-home-stage--landing\n .platform-desktop-shell--workbench\n #platform-tab-panel-create::-webkit-scrollbar',
);
expect(css).toContain('.scrollbar-hide');
expect(css).toContain('::-webkit-scrollbar-thumb');
});
it('uses the platform fill for root background exposed by mobile keyboard shift', () => {
const css = readIndexCss();
const keyboardRootBlock = getCssBlock(
css,
"html[data-mobile-keyboard-open='true'],\nhtml[data-mobile-keyboard-open='true'] body,\nhtml[data-mobile-keyboard-open='true'] #root",
);
expect(keyboardRootBlock).toContain('--platform-keyboard-exposed-fill');
expect(keyboardRootBlock).toContain('#fffdf9');
expect(keyboardRootBlock).not.toContain('#0a0a0a');
});
it('does not globally transform the platform shell while the mobile keyboard is open', () => {
const css = readIndexCss();
const platformShellBlock = getCssBlock(
css,
'.platform-viewport-shell {\n height',
);
expect(platformShellBlock).toContain('--platform-layout-viewport-height');
expect(platformShellBlock).not.toContain('translate3d');
expect(platformShellBlock).not.toContain(
'--platform-keyboard-focus-offset',
);
});
it('uses warm brown tokens for draft unread markers instead of red literals', () => {
const css = readIndexCss();
const themeCss = readPlatformThemeCss();
expect(themeCss).toContain('--platform-unread-dot-fill: #8b5a3d;');
expect(themeCss).toContain(
'--platform-unread-dot-glow: rgba(139, 90, 61, 0.34);',
);
expect(themeCss).toContain('--platform-unread-dot-fill: #d6a27c;');
expect(themeCss).toContain(
'--platform-unread-dot-glow: rgba(214, 162, 124, 0.24);',
);
for (const selector of [
'.creation-work-card__unread-dot',
'.platform-nav-unread-dot',
]) {
const block = getCssBlock(css, selector);
expect(block).toContain('background: var(--platform-unread-dot-fill);');
expect(block).toContain('var(--platform-unread-dot-glow)');
expect(block).not.toContain('#b64a35');
expect(block).not.toContain('rgba(239, 68, 68');
}
});
it('keeps platform font overrides after branded title typography', () => {
const css = readIndexCss();
const themeCss = readPlatformThemeCss();
const titleRuleIndex = css.indexOf('.selection-hero-brand__title');
const platformThemeRuleIndex = css.indexOf(
'.platform-theme,\n.platform-theme *',
);
expect(titleRuleIndex).toBeGreaterThanOrEqual(0);
expect(platformThemeRuleIndex).toBeGreaterThan(titleRuleIndex);
expect(getCssBlock(css, '.platform-theme,\n.platform-theme *')).toContain(
'font-family: var(--platform-font-family) !important;',
);
expect(themeCss).not.toContain('.platform-theme,\n.platform-theme *');
});
it('keeps the creation shelf share button on a visible surface', () => {
const css = readIndexCss();
const block = getCssBlock(css, '.creation-work-card__quick-action-button');
expect(block).toContain('border: 1px solid');
expect(block).toContain('background: color-mix(');
expect(block).toContain('var(--platform-neutral-bg)');
expect(block).toContain('var(--platform-cool-bg)');
expect(block).not.toContain('background: transparent;');
});
it('keeps mini program recommend runtime inside a share snapshot safe area', () => {
const css = readIndexCss();
const block = getCssBlock(
css,
"html[data-wechat-mini-program-runtime='true']\n .platform-recommend-runtime-panel",
);
expect(block).toContain('max-height: min(76dvh, 42rem);');
expect(block).toContain('transform: scale(0.88);');
expect(block).toContain('transform-origin: center;');
});
it('keeps the creation shelf point incentive action button compact', () => {
const css = readIndexCss();
const surfaceBlock = getCssBlock(
css,
'.creation-work-card-incentive__button.platform-button',
);
const compactButtonBlock = getCssBlock(
css,
'.creation-work-card-incentive__button.platform-button {\n display: inline-flex;',
);
const mobileQueryIndex = css.indexOf('@media (max-width: 639px)');
const mobileSelectorIndex = css.indexOf(
'.creation-work-card-incentive__button.platform-button',
mobileQueryIndex,
);
expect(surfaceBlock).toContain('border: 1px solid');
expect(surfaceBlock).toContain('background: color-mix(');
expect(compactButtonBlock).toContain('min-height: 2.55rem;');
expect(compactButtonBlock).toContain('font-size: 0.68rem;');
expect(mobileSelectorIndex).toBeGreaterThanOrEqual(0);
});
});
describe('index stylesheet image canvas asset selection', () => {
it('bounds the raised mobile selection sidebar by the short viewport', () => {
const css = readIndexCss();
const mobileQueryIndex = css.indexOf('@media (max-width: 760px)');
expect(mobileQueryIndex).toBeGreaterThanOrEqual(0);
const selectionSidebarBlock = getCssBlock(
css.slice(mobileQueryIndex),
'.image-canvas-editor__sidebar--asset-selection',
);
expect(selectionSidebarBlock).toContain(
'height: clamp(14rem, 62dvh, 21rem);',
);
expect(selectionSidebarBlock).toContain('max-height: 62dvh;');
expect(selectionSidebarBlock).not.toContain('clamp(19rem');
});
});
describe('index stylesheet draft mobile cards', () => {
it('disables long-press text selection on the whole mobile draft tab', () => {
const css = readIndexCss();
const mobileQueryIndex = css.indexOf('@media (max-width: 639px)');
expect(mobileQueryIndex).toBeGreaterThanOrEqual(0);
const draftPanelSelector =
'#platform-tab-panel-saves,\n #platform-tab-panel-saves *';
const draftPanelSelectorIndex = css.indexOf(
draftPanelSelector,
mobileQueryIndex,
);
expect(draftPanelSelectorIndex).toBeGreaterThanOrEqual(0);
const draftPanelBlock = getCssBlock(css, draftPanelSelector);
expect(draftPanelBlock).toContain('-webkit-user-select: none;');
expect(draftPanelBlock).toContain('user-select: none;');
expect(draftPanelBlock).toContain('-webkit-touch-callout: none;');
const editableSelector =
"#platform-tab-panel-saves :is(input, textarea, [contenteditable='true'])";
const editableSelectorIndex = css.indexOf(
editableSelector,
mobileQueryIndex,
);
expect(editableSelectorIndex).toBeGreaterThanOrEqual(0);
const editableBlock = getCssBlock(css, editableSelector);
expect(editableBlock).toContain('-webkit-user-select: text;');
expect(editableBlock).toContain('user-select: text;');
expect(editableBlock).toContain('-webkit-touch-callout: default;');
});
});
describe('index stylesheet creation agent hero contrast', () => {
it('keeps dark creation progress hero text light inside remap surfaces', () => {
const css = readIndexCss();
expect(css).toContain('.platform-remap-surface');
expect(css).toContain('.creation-agent-hero');
const labelBlock = getCssBlock(
css,
':where(.creation-agent-hero__summary, .creation-agent-hero__progress-label)',
);
expect(labelBlock).toContain('rgba(255, 255, 255, 0.76) !important');
const valueBlock = getCssBlock(css, '.creation-agent-hero__progress-value');
expect(valueBlock).toContain('rgba(255, 255, 255, 0.92) !important');
const hintBlock = getCssBlock(css, '.creation-agent-hero__progress-hint');
expect(hintBlock).toContain('rgba(255, 255, 255, 0.72) !important');
});
});
describe('index stylesheet recommend runtime cover', () => {
it('keeps the card cover above embedded runtime and only fades it when ready', () => {
const css = readIndexCss();
const viewportBlock = getCssBlock(
css,
'.platform-recommend-runtime-viewport',
);
expect(viewportBlock).toContain('z-index: 1;');
expect(viewportBlock).toContain('isolation: isolate;');
const coverBlock = getCssBlock(css, '.platform-recommend-runtime-cover');
expect(coverBlock).toContain('z-index: 30;');
expect(coverBlock).toContain('isolation: isolate;');
expect(coverBlock).not.toContain('transition: opacity');
const loadingBlock = getCssBlock(
css,
'.platform-recommend-runtime-loading',
);
expect(loadingBlock).toContain('position: absolute;');
expect(loadingBlock).toContain('z-index: 4;');
const loadingAnimationBlock = getCssBlock(
css,
'.platform-recommend-runtime-loading::before',
);
expect(loadingAnimationBlock).toContain(
'animation: platform-recommend-runtime-loading 1.15s ease-in-out infinite;',
);
expect(css).toContain('@keyframes platform-recommend-runtime-loading');
const hiddenCoverBlock = getCssBlock(
css,
'.platform-recommend-runtime-cover--hidden',
);
expect(hiddenCoverBlock).toContain('transition: opacity 420ms ease;');
});
});