forked from mirrors/misskey
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
39
.github/workflows/test-frontend.yml
vendored
39
.github/workflows/test-frontend.yml
vendored
|
@ -8,7 +8,44 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
|
|
@ -31,8 +31,8 @@
|
||||||
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
|
||||||
"jest": "cd packages/backend && pnpm jest",
|
"jest": "cd packages/backend && pnpm jest",
|
||||||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||||
"test": "pnpm jest",
|
"test": "pnpm -r test",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage",
|
"test-and-coverage": "pnpm -r test-and-coverage",
|
||||||
"format": "pnpm exec gulp format",
|
"format": "pnpm exec gulp format",
|
||||||
"clean": "node ./scripts/clean.js",
|
"clean": "node ./scripts/clean.js",
|
||||||
"clean-all": "node ./scripts/clean-all.js",
|
"clean-all": "node ./scripts/clean-all.js",
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "vite",
|
"watch": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"test": "vitest --run",
|
||||||
|
"test-and-coverage": "vitest --run --coverage",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
"eslint": "eslint --quiet \"src/**/*.{ts,vue}\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint"
|
"lint": "pnpm typecheck && pnpm eslint"
|
||||||
|
@ -70,6 +72,7 @@
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/vue": "^6.6.1",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
|
@ -85,13 +88,16 @@
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.53.0",
|
"@typescript-eslint/eslint-plugin": "5.53.0",
|
||||||
"@typescript-eslint/parser": "5.53.0",
|
"@typescript-eslint/parser": "5.53.0",
|
||||||
|
"@vitest/coverage-c8": "^0.29.2",
|
||||||
"@vue/runtime-core": "3.2.47",
|
"@vue/runtime-core": "3.2.47",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.7.0",
|
"cypress": "12.7.0",
|
||||||
"eslint": "8.35.0",
|
"eslint": "8.35.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-vue": "9.9.0",
|
"eslint-plugin-vue": "9.9.0",
|
||||||
|
"happy-dom": "8.9.0",
|
||||||
"start-server-and-test": "1.15.4",
|
"start-server-and-test": "1.15.4",
|
||||||
|
"vitest": "^0.29.2",
|
||||||
"vue-eslint-parser": "9.1.0",
|
"vue-eslint-parser": "9.1.0",
|
||||||
"vue-tsc": "1.2.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"/>
|
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
|
||||||
<div :class="$style.hiddenText">
|
<div :class="$style.hiddenText">
|
||||||
<div :class="$style.hiddenTextWrapper">
|
<div :class="$style.hiddenTextWrapper">
|
||||||
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ $ts.sensitive }}</b>
|
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}</b>
|
||||||
<span style="display: block;">{{ $ts.clickToShow }}</span>
|
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<a
|
||||||
:class="$style.imageContainer"
|
:class="$style.imageContainer"
|
||||||
:href="image.url"
|
:href="image.url"
|
||||||
:title="image.name"
|
:title="image.name"
|
||||||
>
|
>
|
||||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
|
<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>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -27,6 +30,7 @@ import * as misskey from 'misskey-js';
|
||||||
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
import { getStaticImageUrl } from '@/scripts/media-proxy';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
image: misskey.entities.DriveFile;
|
image: misskey.entities.DriveFile;
|
||||||
|
@ -34,6 +38,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let hide = $ref(true);
|
let hide = $ref(true);
|
||||||
|
let darkMode = $ref(defaultStore.state.darkMode);
|
||||||
|
|
||||||
const url = (props.raw || defaultStore.state.loadRawImages)
|
const url = (props.raw || defaultStore.state.loadRawImages)
|
||||||
? props.image.url
|
? props.image.url
|
||||||
|
@ -108,18 +113,25 @@ watch(() => props.image, () => {
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gif {
|
.indicators {
|
||||||
background-color: var(--fg);
|
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;
|
border-radius: 6px;
|
||||||
color: var(--accentLighten);
|
color: var(--accentLighten);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
left: 12px;
|
|
||||||
opacity: .5;
|
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
text-align: center;
|
|
||||||
top: 12px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,17 +14,23 @@ import adaptiveBg from './adaptive-bg';
|
||||||
import container from './container';
|
import container from './container';
|
||||||
|
|
||||||
export default function(app: App) {
|
export default function(app: App) {
|
||||||
app.directive('userPreview', userPreview);
|
for (const [key, value] of Object.entries(directives)) {
|
||||||
app.directive('user-preview', userPreview);
|
app.directive(key, value);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 path from 'path';
|
||||||
import pluginVue from '@vitejs/plugin-vue';
|
import pluginVue from '@vitejs/plugin-vue';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import { configDefaults as vitestConfigDefaults } from 'vitest/config';
|
||||||
|
|
||||||
import locales from '../../locales';
|
import locales from '../../locales';
|
||||||
import meta from '../../package.json';
|
import meta from '../../package.json';
|
||||||
|
@ -16,10 +17,10 @@ const hash = (str: string, seed = 0): number => {
|
||||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
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);
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||||
|
|
||||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,12 +29,12 @@ function toBase62(n: number): string {
|
||||||
if (n === 0) {
|
if (n === 0) {
|
||||||
return '0';
|
return '0';
|
||||||
}
|
}
|
||||||
let result = '';
|
let result = '';
|
||||||
while (n > 0) {
|
while (n > 0) {
|
||||||
result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result;
|
result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result;
|
||||||
n = Math.floor(n / BASE62_DIGITS.length);
|
n = Math.floor(n / BASE62_DIGITS.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,5 +111,15 @@ export default defineConfig(({ command, mode }) => {
|
||||||
sourcemap: process.env.NODE_ENV === 'development',
|
sourcemap: process.env.NODE_ENV === 'development',
|
||||||
reportCompressedSize: false,
|
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