diff options
| author | Kagami Sascha Rosylight <saschanaz@outlook.com> | 2023-03-09 04:48:39 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-09 12:48:39 +0900 |
| commit | 4835f0fb4305c43bc0313c7e343c3863b17f435b (patch) | |
| tree | ec10998e9c9b7b8d4388cad69d819514843b39a6 /packages/frontend | |
| parent | fix(client): ロールで広告を無効にするとadmin/adsでプレビュ... (diff) | |
| download | misskey-4835f0fb4305c43bc0313c7e343c3863b17f435b.tar.gz misskey-4835f0fb4305c43bc0313c7e343c3863b17f435b.tar.bz2 misskey-4835f0fb4305c43bc0313c7e343c3863b17f435b.zip | |
fix(frontend): GIFバナーの復活など (#10247)
* Restore GIF banner
* Add ALT banner, detect APNG too
* Add vitest
* Add CI for vitest
* Upload coverage?
* frontend
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/package.json | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkMediaImage.vue | 38 | ||||
| -rw-r--r-- | packages/frontend/src/directives/index.ts | 32 | ||||
| -rw-r--r-- | packages/frontend/test/init.ts | 18 | ||||
| -rw-r--r-- | packages/frontend/test/note.test.ts | 81 | ||||
| -rw-r--r-- | packages/frontend/test/tsconfig.json | 43 | ||||
| -rw-r--r-- | packages/frontend/vite.config.ts | 19 |
7 files changed, 207 insertions, 30 deletions
diff --git a/packages/frontend/package.json b/packages/frontend/package.json index e4c04f5937..add5eabdda 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -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" } diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index b777a1329b..6091b40016 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -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> diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts index 854f0a544e..064ee4f64b 100644 --- a/packages/frontend/src/directives/index.ts +++ b/packages/frontend/src/directives/index.ts @@ -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, +}; diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts new file mode 100644 index 0000000000..96730e7b56 --- /dev/null +++ b/packages/frontend/test/init.ts @@ -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: {}, + }, + }; +}); diff --git a/packages/frontend/test/note.test.ts b/packages/frontend/test/note.test.ts new file mode 100644 index 0000000000..f7c47ec100 --- /dev/null +++ b/packages/frontend/test/note.test.ts @@ -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); + }); +}); diff --git a/packages/frontend/test/tsconfig.json b/packages/frontend/test/tsconfig.json new file mode 100644 index 0000000000..1424fdbdfb --- /dev/null +++ b/packages/frontend/test/tsconfig.json @@ -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", + ] +} diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 89b6dbde25..a90ee55268 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -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', + ], + }, + }, }; }); |