/* * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { describe, test, assert, afterEach, beforeAll, vi } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import './init'; import type { summaly } from 'summaly'; import type * as misskey from 'misskey-js'; import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; type SummalyResult = Awaited>; describe('MkUrlPreview', () => { const renderPreviewBy = async (summary: Partial): Promise => { if (!summary.player) { summary.player = { url: null, width: null, height: null, allow: [], }; } fetchMock.mockOnceIf(/^\/url?/, () => { return { status: 200, body: JSON.stringify(summary), }; }); const result = render(MkUrlPreview, { props: { url: summary.url }, global: { directives, components }, }); await new Promise(resolve => { const observer = new MutationObserver(() => { resolve(); observer.disconnect(); }); observer.observe(result.container, { childList: true, subtree: true }); }); return result; }; const renderAndOpenPreview = async (summary: Partial): Promise => { const mkUrlPreview = await renderPreviewBy(summary); const buttons = mkUrlPreview.getAllByRole('button'); buttons[0].click(); // Wait for the click event to be fired await Promise.resolve(); return mkUrlPreview; }; const renderAndOpenPreviewInIFrame = async (summary: Partial): Promise => { const mkUrlPreview = await renderAndOpenPreview(summary); return mkUrlPreview.container.querySelector('iframe'); }; afterEach(() => { fetchMock.resetMocks(); cleanup(); }); test('Should render the description', async () => { const mkUrlPreview = await renderPreviewBy({ url: 'https://example.local', description: 'Mocked description', }); mkUrlPreview.getByText('Mocked description'); }); test('Having a player should render a button', async () => { const mkUrlPreview = await renderPreviewBy({ url: 'https://example.local', player: { url: 'https://example.local/player', width: null, height: null, allow: [], }, }); const buttons = mkUrlPreview.getAllByRole('button'); assert.strictEqual(buttons.length, 2, 'two buttons'); }); test('Having a player should setup the iframe', async () => { const iframe = await renderAndOpenPreviewInIFrame({ url: 'https://example.local', player: { url: 'https://example.local/player', width: null, height: null, allow: [], }, }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.src, 'https://example.local/player?autoplay=1&auto_play=1'); assert.strictEqual( iframe?.sandbox.toString(), 'allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin', ); }); test('Having a player with `allow` field should set permissions', async () => { const iframe = await renderAndOpenPreviewInIFrame({ url: 'https://example.local', player: { url: 'https://example.local/player', width: null, height: null, allow: ['fullscreen', 'web-share'], }, }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.allow, 'fullscreen;web-share'); }); test('Having a player width should keep the fixed aspect ratio', async () => { const iframe = await renderAndOpenPreviewInIFrame({ url: 'https://example.local', player: { url: 'https://example.local/player', width: 400, height: 200, allow: [], }, }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.parentElement?.style.paddingTop, '50%'); }); test('Having a player width should keep the fixed height', async () => { const iframe = await renderAndOpenPreviewInIFrame({ url: 'https://example.local', player: { url: 'https://example.local/player', width: null, height: 200, allow: [], }, }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.parentElement?.style.paddingTop, '200px'); }); test('Loading a tweet in iframe', async () => { const iframe = await renderAndOpenPreviewInIFrame({ url: 'https://twitter.com/i/web/status/1685072521782325249', }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share'); assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin'); }); test('Loading a post in iframe', async () => { const iframe = await renderAndOpenPreviewInIFrame({ url: 'https://x.com/i/web/status/1685072521782325249', }); assert.exists(iframe, 'iframe should exist'); assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share'); assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin'); }); describe('ActivityPub notes', () => { afterEach(() => { vi.clearAllMocks(); }); test('Preview a note', async () => { vi.mock('@/os', () => { return { api(endpoint: string): unknown { if (endpoint === 'ap/show') { return { type: 'Note', object: { text: 'Mizuki', createdAt: new Date().toISOString(), user: {}, files: [] as misskey.entities.DriveFile[], } as misskey.entities.Note, }; } throw new Error(`Unexpected api call ${endpoint}`); }, }; }); const url = 'https://example.local'; const renderResult = await renderAndOpenPreview({ url, description: 'Misskey', activityPub: url, }); assert.notExists(renderResult.queryByText('Misskey'), 'Original description should disappear'); assert.exists(renderResult.queryByText('Mizuki'), 'ActivityPub fetch result should appear'); }); }); });