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
					
				|  | @ -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, | ||||
| }; | ||||
|  |  | |||
|  | @ -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: {}, | ||||
| 		}, | ||||
| 	}; | ||||
| }); | ||||
|  | @ -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); | ||||
| 	}); | ||||
| }); | ||||
|  | @ -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 New Issue