fix(frontend): GIFバナーの復活など (#10247)
* Restore GIF banner * Add ALT banner, detect APNG too * Add vitest * Add CI for vitest * Upload coverage? * frontend
This commit is contained in:
parent
6607b39235
commit
4835f0fb43
10 changed files with 835 additions and 111 deletions
39
.github/workflows/test-frontend.yml
vendored
39
.github/workflows/test-frontend.yml
vendored
|
@ -8,7 +8,44 @@ on:
|
|||
pull_request:
|
||||
|
||||
jobs:
|
||||
cypress:
|
||||
vitest:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Copy Configure
|
||||
run: cp .github/misskey/test.yml .config
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Test
|
||||
run: pnpm --filter frontend test-and-coverage
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/frontend/coverage/coverage-final.json
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"jest": "cd packages/backend && pnpm jest",
|
||||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||
"test": "pnpm jest",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test": "pnpm -r test",
|
||||
"test-and-coverage": "pnpm -r test-and-coverage",
|
||||
"format": "pnpm exec gulp format",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"clean-all": "node ./scripts/clean-all.js",
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
"scripts": {
|
||||
"watch": "vite",
|
||||
"build": "vite build",
|
||||
"test": "vitest --run",
|
||||
"test-and-coverage": "vitest --run --coverage",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
|
@ -70,6 +72,7 @@
|
|||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/vue": "^6.6.1",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
|
@ -85,13 +88,16 @@
|
|||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.53.0",
|
||||
"@typescript-eslint/parser": "5.53.0",
|
||||
"@vitest/coverage-c8": "^0.29.2",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.7.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"happy-dom": "8.9.0",
|
||||
"start-server-and-test": "1.15.4",
|
||||
"vitest": "^0.29.2",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
"vue-tsc": "1.2.0"
|
||||
}
|
||||
|
|
|
@ -3,21 +3,24 @@
|
|||
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ $ts.sensitive }}</b>
|
||||
<span style="display: block;">{{ $ts.clickToShow }}</span>
|
||||
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.visible" :style="defaultStore.state.darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'">
|
||||
<div v-else :class="$style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'">
|
||||
<a
|
||||
:class="$style.imageContainer"
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
>
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
|
||||
<div v-if="image.type === 'image/gif'" :class="$style.gif">GIF</div>
|
||||
</a>
|
||||
<button v-tooltip="$ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
</div>
|
||||
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -27,6 +30,7 @@ import * as misskey from 'misskey-js';
|
|||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
image: misskey.entities.DriveFile;
|
||||
|
@ -34,6 +38,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
let hide = $ref(true);
|
||||
let darkMode = $ref(defaultStore.state.darkMode);
|
||||
|
||||
const url = (props.raw || defaultStore.state.loadRawImages)
|
||||
? props.image.url
|
||||
|
@ -108,18 +113,25 @@ watch(() => props.image, () => {
|
|||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.gif {
|
||||
background-color: var(--fg);
|
||||
.indicators {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: .5;
|
||||
font-size: 14px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
/* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
|
||||
background-color: black;
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 12px;
|
||||
opacity: .5;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,17 +14,23 @@ import adaptiveBg from './adaptive-bg';
|
|||
import container from './container';
|
||||
|
||||
export default function(app: App) {
|
||||
app.directive('userPreview', userPreview);
|
||||
app.directive('user-preview', userPreview);
|
||||
app.directive('get-size', getSize);
|
||||
app.directive('ripple', ripple);
|
||||
app.directive('tooltip', tooltip);
|
||||
app.directive('hotkey', hotkey);
|
||||
app.directive('appear', appear);
|
||||
app.directive('anim', anim);
|
||||
app.directive('click-anime', clickAnime);
|
||||
app.directive('panel', panel);
|
||||
app.directive('adaptive-border', adaptiveBorder);
|
||||
app.directive('adaptive-bg', adaptiveBg);
|
||||
app.directive('container', container);
|
||||
for (const [key, value] of Object.entries(directives)) {
|
||||
app.directive(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
export const directives = {
|
||||
'userPreview': userPreview,
|
||||
'user-preview': userPreview,
|
||||
'get-size': getSize,
|
||||
'ripple': ripple,
|
||||
'tooltip': tooltip,
|
||||
'hotkey': hotkey,
|
||||
'appear': appear,
|
||||
'anim': anim,
|
||||
'click-anime': clickAnime,
|
||||
'panel': panel,
|
||||
'adaptive-border': adaptiveBorder,
|
||||
'adaptive-bg': adaptiveBg,
|
||||
'container': container,
|
||||
};
|
||||
|
|
18
packages/frontend/test/init.ts
Normal file
18
packages/frontend/test/init.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { vi } from 'vitest';
|
||||
|
||||
// Set i18n
|
||||
import locales from '../../../locales';
|
||||
import { updateI18n } from '@/i18n';
|
||||
updateI18n(locales['en-US']);
|
||||
|
||||
// XXX: misskey-js panics if WebSocket is not defined
|
||||
vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
|
||||
|
||||
// XXX: defaultStore somehow becomes undefined in vitest?
|
||||
vi.mock('@/store.js', () => {
|
||||
return {
|
||||
defaultStore: {
|
||||
state: {},
|
||||
},
|
||||
};
|
||||
});
|
81
packages/frontend/test/note.test.ts
Normal file
81
packages/frontend/test/note.test.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { describe, test, assert, afterEach } from 'vitest';
|
||||
import { render, cleanup, type RenderResult } from '@testing-library/vue';
|
||||
import './init';
|
||||
import type { DriveFile } from 'misskey-js/built/entities';
|
||||
import { directives } from '@/directives';
|
||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||
|
||||
describe('MkMediaImage', () => {
|
||||
const renderMediaImage = (image: Partial<DriveFile>): RenderResult => {
|
||||
return render(MkMediaImage, {
|
||||
props: { image },
|
||||
global: { directives },
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('Attaching JPG should show no indicator', async () => {
|
||||
const mkMediaImage = renderMediaImage({
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
const [gif, alt] = await Promise.all([
|
||||
mkMediaImage.queryByText('GIF'),
|
||||
mkMediaImage.queryByText('ALT'),
|
||||
]);
|
||||
assert.ok(!gif);
|
||||
assert.ok(!alt);
|
||||
});
|
||||
|
||||
test('Attaching GIF should show a GIF indicator', async () => {
|
||||
const mkMediaImage = renderMediaImage({
|
||||
type: 'image/gif',
|
||||
});
|
||||
const [gif, alt] = await Promise.all([
|
||||
mkMediaImage.queryByText('GIF'),
|
||||
mkMediaImage.queryByText('ALT'),
|
||||
]);
|
||||
assert.ok(gif);
|
||||
assert.ok(!alt);
|
||||
});
|
||||
|
||||
test('Attaching APNG should show a GIF indicator', async () => {
|
||||
const mkMediaImage = renderMediaImage({
|
||||
type: 'image/apng',
|
||||
});
|
||||
const [gif, alt] = await Promise.all([
|
||||
mkMediaImage.queryByText('GIF'),
|
||||
mkMediaImage.queryByText('ALT'),
|
||||
]);
|
||||
assert.ok(gif);
|
||||
assert.ok(!alt);
|
||||
});
|
||||
|
||||
test('Attaching image with an alt message should show an ALT indicator', async () => {
|
||||
const mkMediaImage = renderMediaImage({
|
||||
type: 'image/png',
|
||||
comment: 'Misskeyのロゴです',
|
||||
});
|
||||
const [gif, alt] = await Promise.all([
|
||||
mkMediaImage.queryByText('GIF'),
|
||||
mkMediaImage.queryByText('ALT'),
|
||||
]);
|
||||
assert.ok(!gif);
|
||||
assert.ok(alt);
|
||||
});
|
||||
|
||||
test('Attaching GIF image with an alt message should show a GIF and an ALT indicator', async () => {
|
||||
const mkMediaImage = renderMediaImage({
|
||||
type: 'image/gif',
|
||||
comment: 'Misskeyのロゴです',
|
||||
});
|
||||
const [gif, alt] = await Promise.all([
|
||||
mkMediaImage.queryByText('GIF'),
|
||||
mkMediaImage.queryByText('ALT'),
|
||||
]);
|
||||
assert.ok(gif);
|
||||
assert.ok(alt);
|
||||
});
|
||||
});
|
43
packages/frontend/test/tsconfig.json
Normal file
43
packages/frontend/test/tsconfig.json
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmitOnError": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"target": "es2021",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"../node_modules/@types",
|
||||
],
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
],
|
||||
"types": ["node"]
|
||||
},
|
||||
"compileOnSave": false,
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"../src/**/*.vue",
|
||||
]
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import path from 'path';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import { defineConfig } from 'vite';
|
||||
import { configDefaults as vitestConfigDefaults } from 'vitest/config';
|
||||
|
||||
import locales from '../../locales';
|
||||
import meta from '../../package.json';
|
||||
|
@ -16,10 +17,10 @@ const hash = (str: string, seed = 0): number => {
|
|||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
|
||||
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
|
||||
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
};
|
||||
|
||||
|
@ -28,12 +29,12 @@ function toBase62(n: number): string {
|
|||
if (n === 0) {
|
||||
return '0';
|
||||
}
|
||||
let result = '';
|
||||
let result = '';
|
||||
while (n > 0) {
|
||||
result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result;
|
||||
n = Math.floor(n / BASE62_DIGITS.length);
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -110,5 +111,15 @@ export default defineConfig(({ command, mode }) => {
|
|||
sourcemap: process.env.NODE_ENV === 'development',
|
||||
reportCompressedSize: false,
|
||||
},
|
||||
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
deps: {
|
||||
inline: [
|
||||
// XXX: misskey-dev/browser-image-resizer has no "type": "module"
|
||||
'browser-image-resizer',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
666
pnpm-lock.yaml
666
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue