summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-03-09 04:48:39 +0100
committerGitHub <noreply@github.com>2023-03-09 12:48:39 +0900
commit4835f0fb4305c43bc0313c7e343c3863b17f435b (patch)
treeec10998e9c9b7b8d4388cad69d819514843b39a6 /packages/frontend
parentfix(client): ロールで広告を無効にするとadmin/adsでプレビュ... (diff)
downloadmisskey-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.json6
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue38
-rw-r--r--packages/frontend/src/directives/index.ts32
-rw-r--r--packages/frontend/test/init.ts18
-rw-r--r--packages/frontend/test/note.test.ts81
-rw-r--r--packages/frontend/test/tsconfig.json43
-rw-r--r--packages/frontend/vite.config.ts19
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',
+ ],
+ },
+ },
};
});