matrix-art

An image gallery for Matrix
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-art.git
Log | Files | Refs | README | LICENSE

commit fa8daa4e7cdf50ed9ad714164e733c8ea6cad03b
parent e2c558eee2f91e87d2667c84d36c55980cd1fe01
Author: MTRNord <mtrnord1@gmail.com>
Date:   Mon,  7 Feb 2022 20:51:41 +0100

Mock out client in tests to not run into race conditions causing wrong state due to timeout.

Diffstat:
A.env.test | 2++
Mcomponents/FrontPageImage.tsx | 3+--
Mcomponents/Header.tsx | 35++++++++++++-----------------------
Me2e/checkEnglishWorks.spec.ts | 2++
Me2e/navigateToDetails.spec.ts | 5+++--
Mhelpers/matrix_client.ts | 139++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackage.json | 13++++++-------
Mpages/api/directory.ts | 11+++++------
Mpages/post/[id].tsx | 3++-
Mpages/profile/[userid].tsx | 5++---
Mplaywright.config.ts | 2+-
11 files changed, 150 insertions(+), 70 deletions(-)

diff --git a/.env.test b/.env.test @@ -0,0 +1 @@ +NEXT_PUBLIC_ENV="test" +\ No newline at end of file diff --git a/components/FrontPageImage.tsx b/components/FrontPageImage.tsx @@ -58,7 +58,7 @@ export default class FrontPageImage extends PureComponent<Props, State> { try { const profile = await this.context.client.getProfile(this.props.event.sender); this.setState({ - displayname: profile.displayname, + displayname: profile.displayname || this.props.event.sender, }); } catch (error) { console.debug(`Failed to fetch profile for user ${this.props.event.sender}:`, error); @@ -68,7 +68,6 @@ export default class FrontPageImage extends PureComponent<Props, State> { } async registerAsGuest() { - console.log(this.context.is_generating_guest); if (this.context.is_generating_guest) { return; } diff --git a/components/Header.tsx b/components/Header.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Component } from "react"; +import { PureComponent } from "react"; import User from "../helpers/db/Users"; import { ClientContext } from "./ClientContext"; import { i18n } from 'next-i18next'; @@ -12,30 +12,18 @@ type State = { directory_data: User[]; loading: boolean; error?: string; - loggedIn?: boolean; }; -export default class Header extends Component<Props, State> { +export default class Header extends PureComponent<Props, State> { declare context: React.ContextType<typeof ClientContext>; constructor(props: Props) { super(props); this.state = { directory_data: [], - loading: true, - loggedIn: undefined + loading: true } as State; } - shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<State>, nextContext: React.ContextType<typeof ClientContext>) { - if (typeof window !== "undefined" && (nextState.loggedIn === undefined || nextState.loggedIn !== !nextContext.client.isGuest)) { - return true; - } - if (nextState !== this.state || nextContext !== this.context) { - return true; - } - return false; - } - componentDidUpdate(prevProps: Props, prevState: State) { if (this.state.error && this.state.error !== prevState.error) { toast(() => <div><h2 className="text-xl text-white">{i18n?.t("Error")}</h2><br />{this.state.error}</div>, { @@ -59,7 +47,8 @@ export default class Header extends Component<Props, State> { if (this.state.loading) { return <></>; } - const loggedIn = (typeof window === "undefined") ? undefined : this.context.client.isGuest !== undefined ? (!this.context.client.isGuest) : undefined; + const loggedIn = (typeof window === "undefined") ? undefined : !this.context.client.isGuest; + console.log("loggedIn:", loggedIn); return ( <> <header className='bg-[#f8f8f8] dark:bg-[#06070D] flex fixed top-0 left-0 right-0 lg:h-20 h-auto z-[100] items-center lg:flex-row flex-col shadow-black drop-shadow-xl'> @@ -91,19 +80,19 @@ export default class Header extends Component<Props, State> { </div> <nav className='flex lg:flex-shrink-0 my-4'> - {!loggedIn ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/register">{i18n?.t('Join')}</Link></span> : undefined} - {!loggedIn ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/login">{i18n?.t('Log in')}</Link></span> : undefined} - {loggedIn && this.state.directory_data.some(thing => thing.mxid == this.context.client.userId) ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href={"/profile/" + encodeURIComponent(this.context.client.userId!)}>{i18n?.t('Profile')}</Link></span> : undefined} - {loggedIn ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/logout/">{i18n?.t('Logout')}</Link></span> : undefined} + {loggedIn !== undefined && !loggedIn ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/register">{i18n?.t('Join')}</Link></span> : undefined} + {loggedIn !== undefined && !loggedIn ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/login">{i18n?.t('Log in')}</Link></span> : undefined} + {loggedIn !== undefined && loggedIn && this.state.directory_data.some(thing => thing.mxid == this.context.client.userId) ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href={"/profile/" + encodeURIComponent(this.context.client.userId!)}>{i18n?.t('Profile')}</Link></span> : undefined} + {loggedIn !== undefined && loggedIn ? <span className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all'><Link href="/logout/">{i18n?.t('Logout')}</Link></span> : undefined} </nav> </div> - {loggedIn ? <span className='lg:opacity-100 opacity-0 inline-block bg-gray-900 dark:bg-gray-200 w-[1px] lg:h-7 h-0'></span> : <span className="mr-4"></span>} + {loggedIn !== undefined && loggedIn ? <span className='lg:opacity-100 opacity-0 inline-block bg-gray-900 dark:bg-gray-200 w-[1px] lg:h-7 h-0'></span> : <span className="mr-4"></span>} <div className='relative lg:m-0'> <div className='flex'> { this.state.directory_data.some(thing => thing.mxid == this.context.client.userId) ? - (loggedIn ? <Link href="/submit"><a className='inline-flex justify-center items-center text-teal-400 hover:text-teal-200 bg-transparent relative h-14 min-w-[9.25rem] z-[2] cursor-pointer font-bold'>{i18n?.t('Submit')}</a></Link> : undefined) : - (loggedIn ? <a className='inline-flex justify-center items-center text-teal-400 hover:text-teal-200 bg-transparent relative h-14 min-w-[9.25rem] z-[2] cursor-pointer font-bold'>{i18n?.t('Setup Account')}</a> : undefined) + (loggedIn !== undefined && loggedIn ? <Link href="/submit"><a className='inline-flex justify-center items-center text-teal-400 hover:text-teal-200 bg-transparent relative h-14 min-w-[9.25rem] z-[2] cursor-pointer font-bold'>{i18n?.t('Submit')}</a></Link> : undefined) : + (loggedIn !== undefined && loggedIn ? <a className='inline-flex justify-center items-center text-teal-400 hover:text-teal-200 bg-transparent relative h-14 min-w-[9.25rem] z-[2] cursor-pointer font-bold'>{i18n?.t('Setup Account')}</a> : undefined) } </div> </div> diff --git a/e2e/checkEnglishWorks.spec.ts b/e2e/checkEnglishWorks.spec.ts @@ -8,12 +8,14 @@ test('test if we have english words', async ({ page, baseURL }) => { page.on('console', msg => console.log(msg.text())); // Go to http://localhost:3000/ await page.goto(baseURL || 'http://localhost:3000/'); + await page.waitForLoadState("networkidle"); expect(await page.locator("text=Home").isVisible()); }); test('test if the header is there and english with correct state', async ({ page, baseURL }) => { page.on('console', msg => console.log(msg.text())); + await page.waitForLoadState("networkidle"); // Go to http://localhost:3000/ await page.goto(baseURL || 'http://localhost:3000/'); diff --git a/e2e/navigateToDetails.spec.ts b/e2e/navigateToDetails.spec.ts @@ -6,6 +6,7 @@ test.use({ test('test navigation to details', async ({ page, baseURL }) => { page.on('console', msg => console.log(msg.text())); + await page.waitForLoadState("networkidle"); // Go to http://localhost:3000/ await page.goto(baseURL || "http://localhost:3000"); @@ -14,11 +15,11 @@ test('test navigation to details', async ({ page, baseURL }) => { // Click text=Flowers@mtrnord:art.midnightthoughts.space await Promise.all([ page.waitForNavigation(/*{ url: 'http://localhost:3000/post/%24ugLG5srr5AyYCIhL1CnD6KikH8QYsDVMUHQ9jQRn990' }*/), - page.click('text=Flowers') + page.click('text=Test') ]); - await expect(page).toHaveURL(`${baseURL || "http://localhost:3000"}/post/%24xoIMe7tUMb2NhCBZaxsZr2CVkptCVu1GaJ_eJMKbJQo`); + await expect(page).toHaveURL(`${baseURL || "http://localhost:3000"}/post/%24QtbB-3JYAEOXJeC-mrZqFIEqon4uBLYVwTSw2SDWrJg`); // Click svg diff --git a/helpers/matrix_client.ts b/helpers/matrix_client.ts @@ -20,13 +20,11 @@ export default class MatrixClient { get accessToken(): string | undefined { return this._accessToken; } - get isGuest(): boolean | undefined { - return this._isGuest; + get isGuest(): boolean { + return this._isGuest === undefined ? true : this._isGuest; } get profileRoomId(): string | undefined { - console.log(this.joinedRooms); - console.log(this._profileRoomId); return this.joinedRooms.get(`#${this.userId}`) || this._profileRoomId; } @@ -50,10 +48,10 @@ export default class MatrixClient { return; } this.storage.setOrDelete("serverUrl", this.serverUrl); - this.storage.setOrDelete("userId", this.userId); - this.storage.setOrDelete("accessToken", this.accessToken); + this.storage.setOrDelete("userId", this._userId); + this.storage.setOrDelete("accessToken", this._accessToken); this.storage.setOrDelete("serverName", this.serverName); - this.storage.setOrDelete("isGuest", this.isGuest); + this.storage.setOrDelete("isGuest", this._isGuest); } private generateToken(len: number) { @@ -75,20 +73,28 @@ export default class MatrixClient { let password = this.generateToken(32); let data; - try { - data = await this.fetchJson(`${serverUrl}/r0/register?kind=guest`, { - method: "POST", - body: JSON.stringify({ - auth: { - type: "m.login.dummy", - }, - username: username, - password: password, - }), - }); - } catch (error) { - console.error(`${serverUrl}/r0/register?kind=guest:\n${error}`); - throw new Error("Failed to register new guest"); + if (process.env.NEXT_PUBLIC_ENV !== "test") { + try { + data = await this.fetchJson(`${serverUrl}/r0/register?kind=guest`, { + method: "POST", + body: JSON.stringify({ + auth: { + type: "m.login.dummy", + }, + username: username, + password: password, + }), + }); + } catch (error) { + console.error(`${serverUrl}/r0/register?kind=guest:\n${error}`); + throw new Error("Failed to register new guest"); + } + } else { + data = { + user_id: "1", + access_token: "1", + home_server: "https://blub", + }; } this.serverUrl = serverUrl; this._userId = data.user_id; @@ -120,6 +126,11 @@ export default class MatrixClient { } private async fetchJson(fullUrl: string, fetchParams: any) { + // Do not execute requests in tests! + if (process.env.NEXT_PUBLIC_ENV === "test") { + return; + } + const response = await fetch(fullUrl, fetchParams); const data = await response.json(); if (response.status === 429) { @@ -167,7 +178,7 @@ export default class MatrixClient { body: "{}", headers: { Authorization: `Bearer ${this.accessToken}` }, }); - return data.access_token; + return process.env.NEXT_PUBLIC_ENV !== "test" ? data.access_token : "openid"; } async register(serverUrl: string, username: string, password: string) { @@ -200,6 +211,17 @@ export default class MatrixClient { } async setDisplayname(newDisplayname: string) { + if (process.env.NEXT_PUBLIC_ENV === "test") { + if (this.userProfileCache.has(this.userId!)) { + const old = this.userProfileCache.get(this.userId!); + old.displayname = newDisplayname; + this.userProfileCache.set(this.userId!, old); + } else { + await this.getProfile(this.userId!); + } + return; + } + await this.fetchJson( `${this.serverUrl}/r0/profile/${encodeURIComponent(this.userId!)}/displayname`, { @@ -218,6 +240,17 @@ export default class MatrixClient { } async setAvatarUrl(newAvatarUrl: string) { + if (process.env.NEXT_PUBLIC_ENV === "test") { + if (this.userProfileCache.has(this.userId!)) { + const old = this.userProfileCache.get(this.userId!); + old.avatar_url = newAvatarUrl; + this.userProfileCache.set(this.userId!, old); + } else { + await this.getProfile(this.userId!); + } + return; + } + await this.fetchJson( `${this.serverUrl}/r0/profile/${encodeURIComponent(this.userId!)}/avatar_url`, { @@ -235,7 +268,14 @@ export default class MatrixClient { } } - async getProfile(userId: string) { + async getProfile(userId: string): Promise<{ displayname?: string; avatar_url?: string; }> { + if (process.env.NEXT_PUBLIC_ENV === "test") { + const data = { + displayname: "Test" + }; + this.userProfileCache.set(userId, data); + return data; + } if (this.userProfileCache.has(userId)) { console.debug(`Returning cached copy of ${userId}'s profile`); return this.userProfileCache.get(userId); @@ -258,7 +298,10 @@ export default class MatrixClient { * @param {string} roomAlias The room alias to join * @returns {string} The room ID of the joined room. */ - async joinProfileRoom(roomAlias: string) { + async joinProfileRoom(roomAlias: string): Promise<string> { + if (process.env.NEXT_PUBLIC_ENV === "test") { + return "!1:blub"; + } const roomId = this.joinedRooms.get(roomAlias); if (roomId) { return roomId; @@ -352,6 +395,9 @@ export default class MatrixClient { } async sendEvent(roomId: string, event_type: string, content: MatrixContents): Promise<string> { + if (process.env.NEXT_PUBLIC_ENV === "test") { + return "$abcde"; + } const txnId = Date.now(); const data = await this.fetchJson( `${this.serverUrl}/r0/rooms/${encodeURIComponent( @@ -367,6 +413,9 @@ export default class MatrixClient { } async uploadFile(file: File | Blob): Promise<string> { + if (process.env.NEXT_PUBLIC_ENV === "test") { + return "mxc://blub/blub"; + } const fileName = (file as File).name || Date.now(); const mediaUrl = this.serverUrl?.slice(0, -1 * "/client".length); const res = await fetch( @@ -456,7 +505,47 @@ export default class MatrixClient { return info; } - async getTimeline(roomId: string, limit: number, filter: object = { limit: 30, types: ["m.image", "m.image_gallery"] }) {// eslint-disable-line unicorn/no-object-as-default-parameter + async getTimeline(roomId: string, limit: number, filter: object = { limit: 30, types: ["m.image", "m.image_gallery"] }): Promise<MatrixEvent[]> {// eslint-disable-line unicorn/no-object-as-default-parameter + if (process.env.NEXT_PUBLIC_ENV === "test") { + return [ + { + type: "m.image", + room_id: '!hxnnsrGMUfcrXPEPZu:art.midnightthoughts.space', + event_id: '$QtbB-3JYAEOXJeC-mrZqFIEqon4uBLYVwTSw2SDWrJg', + origin_server_ts: 1643317935965, + content: { + "m.caption": [ + { + "m.text": "Test2" + } + ], + "m.thumbnail": [ + { + "height": 500, + "mimetype": "image/jpeg", + "size": 126199, + "url": "mxc://art.midnightthoughts.space/b73141659586a6d690fe2c3edd43776b19f51529", + "width": 800 + } + ], + 'm.file': { + mimetype: 'image/png', + name: '767-F_xp11 - 2021-04-13 20.23.07.png', + size: 2129309, + url: 'mxc://art.midnightthoughts.space/21a2b8190b756259510617d3c4c662d1e7c82141' + }, + 'm.image': { height: 1040, width: 1920 }, + 'm.text': 'Test', + 'matrixart.description': 'test2', + 'matrixart.license': 'cc-by-4.0', + 'matrixart.nsfw': false, + 'matrixart.tags': ['test', 'test2'], + 'xyz.amorgan.blurhash': 'LJA0tGWES2oL~pWDayj[.8WBsAbH' + }, + sender: '@test:art.midnightthoughts.space' + } + ]; + } if (!this.accessToken) { console.error("No access token"); return []; diff --git a/package.json b/package.json @@ -5,14 +5,13 @@ "cover 99.9%" ], "scripts": { - "dev-inspect": "cross-env NODE_OPTIONS='--inspect' next dev", - "dev-tests": "cross-env PLAYWRIGHT='1' next dev", "dev": "next dev", - "build": "next build", - "start": "next start", - "export": "next export", - "lint": "next lint", - "extractKeys": "i18next 'pages/**/*.{ts,tsx}' 'components/**/*.{ts,tsx}'" + "build": "cross-env NODE_ENV='production' next build", + "start": "cross-env NODE_ENV='production' next start", + "export": "cross-env NODE_ENV='production' next export", + "lint": "cross-env NODE_ENV='production' next lint", + "extractKeys": "i18next 'pages/**/*.{ts,tsx}' 'components/**/*.{ts,tsx}'", + "test": "cross-env NODE_ENV='test' playwright test" }, "engines": { "node": ">=17.0.0" diff --git a/pages/api/directory.ts b/pages/api/directory.ts @@ -22,12 +22,11 @@ const db = new Sequelize({ db.addModels([User]); export const get_data = async () => { - if (process.env.PLAYWRIGHT === '1') { - console.log("Running in tests!"); + if (process.env.NEXT_PUBLIC_ENV === "test") { return [ new User({ - "mxid": "@mtrnord:art.midnightthoughts.space", - "public_user_room": "#@mtrnord:art.midnightthoughts.space" + "mxid": "@test:art.midnightthoughts.space", + "public_user_room": "#@test:art.midnightthoughts.space" }) ]; } @@ -66,7 +65,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) user_room: string; access_token: string; } = JSON.parse(req.body); - if (await (new ServerOpenID().verify(data.user_id, data.access_token))) { + if (process.env.NEXT_PUBLIC_ENV === "test" ? data.access_token === "openid" : await (new ServerOpenID().verify(data.user_id, data.access_token))) { try { await User.sync(); await User.create({ @@ -89,7 +88,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) user_id: string; access_token: string; } = JSON.parse(req.body); - if (await (new ServerOpenID().verify(data.user_id, data.access_token))) { + if (process.env.NEXT_PUBLIC_ENV === "test" ? data.access_token === "openid" : await (new ServerOpenID().verify(data.user_id, data.access_token))) { try { await User.sync(); await User.destroy({ diff --git a/pages/post/[id].tsx b/pages/post/[id].tsx @@ -114,7 +114,7 @@ class Post extends PureComponent<Props, State> { const profile = await client.getProfile(image_event.sender); this.setState({ image_event: image_event as MatrixImageEvents, - displayname: profile.displayname, + displayname: profile.displayname || image_event.sender, }); } catch (error) { @@ -491,6 +491,7 @@ export const getServerSideProps: GetServerSideProps = async ({ res, locale, quer roomId = await client?.followUser(user.public_user_room); } catch { console.error("Unbable to join room"); + continue; } const events = await client?.getTimeline(roomId, 100); // Filter events by type diff --git a/pages/profile/[userid].tsx b/pages/profile/[userid].tsx @@ -22,7 +22,7 @@ type Props = InferGetServerSidePropsType<typeof getServerSideProps> & { type State = { displayname: string; - avatar_url: string; + avatar_url?: string; events: MatrixEvent[] | []; profile_event?: MatrixArtProfile; error?: string; @@ -41,7 +41,6 @@ class Profile extends PureComponent<Props, State> { this.state = { displayname: this.props.mxid, - avatar_url: "", events: [], isLoadingImages: false, hasFullyLoaded: false, @@ -83,7 +82,7 @@ class Profile extends PureComponent<Props, State> { try { const profile = await this.context.client.getProfile(this.props.mxid); this.setState({ - displayname: profile.displayname, + displayname: profile.displayname || this.props.event.sender, avatar_url: profile.avatar_url, }); } catch (error) { diff --git a/playwright.config.ts b/playwright.config.ts @@ -106,7 +106,7 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { - command: 'npm run dev-tests', + command: 'npm run dev', port: 3000, }, };