From abcf56dd723064f4b6346f18d1d95ea231696aaf Mon Sep 17 00:00:00 2001 From: Alain Bourgeois Date: Wed, 20 May 2026 19:56:08 +0200 Subject: [PATCH 1/8] Map solid-logic to local source in Jest Mock @uvdsl/solid-oidc-client-browser for JSDOM tests Keep TypeScript config stable for linked workspace development Preserve pane behavior while upstream auth/session contracts change --- jest.config.mjs | 4 +- src/mainPage/menu.ts | 2 +- test/mocks/solid-oidc-client-browser.ts | 57 +++++++++++++++++++++++++ tsconfig.json | 14 +++++- 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 test/mocks/solid-oidc-client-browser.ts diff --git a/jest.config.mjs b/jest.config.mjs index 7d1c1632..a9873594 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -19,7 +19,9 @@ export default { '\\.svg\\?raw$': '/test/__mocks__/fileMock.js', '\\.(svg)$': '/test/__mocks__/fileMock.js', '\\.(png|jpe?g|gif|webp|avif)$': '/test/__mocks__/fileMock.js', - '\\.css$': '/test/__mocks__/styleMock.js' + '\\.css$': '/test/__mocks__/styleMock.js', + 'solid-logic': '/../solid-logic/src/index.ts', + 'solid-oidc-client-browser': '/test/mocks/solid-oidc-client-browser.ts' }, setupFilesAfterEnv: ['./test/helpers/setup.ts'], testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], diff --git a/src/mainPage/menu.ts b/src/mainPage/menu.ts index 028a3fdc..c978cb98 100644 --- a/src/mainPage/menu.ts +++ b/src/mainPage/menu.ts @@ -59,7 +59,7 @@ const applyMenuCollapsedState = (navMenu: HTMLElement | null): void => { updateCollapseButtonPosition(navMenu, collapseBtn) } -const isLoggedIn = (): boolean => Boolean(authSession?.info?.isLoggedIn) +const isLoggedIn = (): boolean => Boolean(authSession?.isActive) const ensureMenuSkeleton = () => { menuCollapsed = loadMenuCollapsedState() diff --git a/test/mocks/solid-oidc-client-browser.ts b/test/mocks/solid-oidc-client-browser.ts new file mode 100644 index 00000000..69af6043 --- /dev/null +++ b/test/mocks/solid-oidc-client-browser.ts @@ -0,0 +1,57 @@ +type Listener = (...args: any[]) => void + +class EventEmitterLike { + private listeners: Record = {} + + on(event: string, listener: Listener): void { + const list = this.listeners[event] || [] + list.push(listener) + this.listeners[event] = list + } + + emit(event: string, ...args: any[]): void { + const list = this.listeners[event] || [] + list.forEach(listener => listener(...args)) + } +} + +export class Session { + info: { webId?: string, isLoggedIn: boolean } = { isLoggedIn: false } + webId?: string + isActive = false + events = new EventEmitterLike() + + addEventListener(event: string, listener: Listener): void { + this.events.on(event, listener) + } + + async handleIncomingRedirect(): Promise { + return + } + + async handleRedirectFromLogin(): Promise { + return + } + + async restore(): Promise { + return + } + + async login(): Promise { + return + } + + async logout(): Promise { + this.info = { isLoggedIn: false } + this.webId = undefined + this.isActive = false + } + + fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + return globalThis.fetch(input, init) + } + + authFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + return globalThis.fetch(input, init) + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f3ba4d21..944efc7f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -57,8 +57,18 @@ ] /* List of folders to include type definitions from. */, // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "preserveSymlinks": true, /* Keep symlinks unresolved to avoid duplicate rdflib type identities in local linked dev. */ + /* baseUrl + paths below are for local integrated dev only. + They force a single rdflib type identity when solid-panes is linked to local solid-logic/solid-ui. + Both options are deprecated in TS6+ but remain functional until TS7. + See solid-ui README: Local Integrated Development for context. */ + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "rdflib": ["node_modules/rdflib"], + "rdflib/*": ["node_modules/rdflib/*"] + } /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ From ad135e7793af981ba35f90dcbbc34a8c2928765b Mon Sep 17 00:00:00 2001 From: Alain Bourgeois Date: Fri, 22 May 2026 11:03:37 +0200 Subject: [PATCH 2/8] uvdsl oidc client --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8aea1923..e3496ea8 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ so typically a hand-written user interface will be chosen over a generic machine These panes are used in the Data Browser - see mashlib [https://github.com/linkeddata/mashlib](https://github.com/linkeddata/mashlib) +When panes are hosted through mashlib and use solid-logic authentication, ensure the refresh worker is served same-origin. The worker export and runtime override contract are documented in solid-logic: +https://github.com/solidos/solid-logic#worker-asset-and-runtime-configuration + Currently the panes available include: - A default pane which lists the properties of any object From 54812314050b4e57ae6a4028e024cc4f6c38f28f Mon Sep 17 00:00:00 2001 From: Alain Bourgeois Date: Sat, 23 May 2026 19:45:37 +0200 Subject: [PATCH 3/8] fix(dev): use authn compatibility login flow Route dev loader authentication through authn.checkUser instead of direct session redirect handling.\nUse compatibility-safe session state checks for banner rendering and login flow. --- dev/loader.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dev/loader.ts b/dev/loader.ts index 71984bfc..a22f552f 100644 --- a/dev/loader.ts +++ b/dev/loader.ts @@ -129,22 +129,22 @@ window.onload = async () => { console.log('document ready') // registerPanes((cjsOrEsModule: any) => paneRegistry.register(cjsOrEsModule.default || cjsOrEsModule)) paneRegistry.register(require('contacts-pane')) - await authSession.handleIncomingRedirect({ - restorePreviousSession: true - }) - const session = await authSession - if (!session.info.isLoggedIn) { + await solidLogicSingleton.authn.checkUser() + const session = authSession + const isLoggedIn = session?.info?.isLoggedIn ?? session?.isActive ?? Boolean(session?.webId) + if (!isLoggedIn) { console.log('The user is not logged in') const loginBanner = document.getElementById('loginBanner'); if (loginBanner) { loginBanner.innerHTML = ''; } } else { - console.log(`Logged in as ${session.info.webId}`) + const loggedWebId = session?.info?.webId || session?.webId + console.log(`Logged in as ${loggedWebId}`) const loginBanner = document.getElementById('loginBanner'); if (loginBanner) { - loginBanner.innerHTML = `Logged in as ${session.info.webId} `; + loginBanner.innerHTML = `Logged in as ${loggedWebId} `; } } addLayoutButtons() @@ -155,8 +155,9 @@ window.logout = () => { window.location.href = '' } window.login = async function () { - const session = await authSession - if (!session.info.isLoggedIn) { + const session = authSession + const isLoggedIn = session?.info?.isLoggedIn ?? session?.isActive ?? Boolean(session?.webId) + if (!isLoggedIn) { const issuer = prompt('Please enter an issuer URI', 'https://solidcommunity.net') if (issuer) { await authSession.login({ From b2e4ac59628ab1d3150a9285ca02e2a69e0fd975 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 29 May 2026 12:03:07 +0200 Subject: [PATCH 4/8] fix(header): prevent auth flicker and support session shape variants --- src/mainPage/header.ts | 28 ++++++++++++++++++++++++++++ src/mainPage/menu.ts | 6 +++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/mainPage/header.ts b/src/mainPage/header.ts index f6546e7f..b525c268 100644 --- a/src/mainPage/header.ts +++ b/src/mainPage/header.ts @@ -38,6 +38,33 @@ export const HELP_MENU_LIST = [ ] const HEADER_MOBILE_STYLE_ID = 'solid-ui-header-mobile-style' +let authRefreshInFlight: Promise | null = null + +async function ensureAuthUserResolved (): Promise { + if (authn.currentUser()) return + if (authRefreshInFlight) { + await authRefreshInFlight + return + } + + authRefreshInFlight = (async () => { + try { + await authn.checkUser() + // Some auth stacks resolve session state asynchronously after first check. + if (!authn.currentUser()) { + await authn.checkUser() + } + } catch (_err) { + // Keep header rendering resilient when auth refresh fails. + } + })() + + try { + await authRefreshInFlight + } finally { + authRefreshInFlight = null + } +} type ManagedHeader = Header & { __solidPanesListenersAttached?: boolean @@ -139,6 +166,7 @@ function attachHeaderListeners (header: ManagedHeader) { export async function refreshHeader (outliner: OutlineManager, headerElement?: Header) { ensureMobileHeaderStyles() + await ensureAuthUserResolved() const headerOptions = setHeaderOptions(outliner) const header = headerElement || document.querySelector('solid-ui-header') as Header | null if (!header) return null diff --git a/src/mainPage/menu.ts b/src/mainPage/menu.ts index 872c44f5..a6261f11 100644 --- a/src/mainPage/menu.ts +++ b/src/mainPage/menu.ts @@ -59,7 +59,11 @@ const applyMenuCollapsedState = (navMenu: HTMLElement | null): void => { updateCollapseButtonPosition(navMenu, collapseBtn) } -const isLoggedIn = (): boolean => Boolean(authSession?.isActive) +// Compatibility: solid-logic Session shape differs across stacks (info.isLoggedIn vs isActive/webId). +const isLoggedIn = (): boolean => { + const sessionAny = authSession as any + return Boolean(sessionAny?.info?.isLoggedIn ?? sessionAny?.isActive ?? sessionAny?.webId) +} const setFooterVisibility = (loggedIn: boolean): void => { const footer = document.querySelector('solid-ui-footer') as HTMLElement | null From 050d93dbfc0c3fe0f0c2bd3e9f84fc8b599fa31e Mon Sep 17 00:00:00 2001 From: Alain Bourgeois Date: Fri, 29 May 2026 15:31:12 +0200 Subject: [PATCH 5/8] lint --fix --- test/mocks/solid-oidc-client-browser.ts | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/mocks/solid-oidc-client-browser.ts b/test/mocks/solid-oidc-client-browser.ts index 69af6043..009b061b 100644 --- a/test/mocks/solid-oidc-client-browser.ts +++ b/test/mocks/solid-oidc-client-browser.ts @@ -3,13 +3,13 @@ type Listener = (...args: any[]) => void class EventEmitterLike { private listeners: Record = {} - on(event: string, listener: Listener): void { + on (event: string, listener: Listener): void { const list = this.listeners[event] || [] list.push(listener) this.listeners[event] = list } - emit(event: string, ...args: any[]): void { + emit (event: string, ...args: any[]): void { const list = this.listeners[event] || [] list.forEach(listener => listener(...args)) } @@ -21,37 +21,37 @@ export class Session { isActive = false events = new EventEmitterLike() - addEventListener(event: string, listener: Listener): void { + addEventListener (event: string, listener: Listener): void { this.events.on(event, listener) } - async handleIncomingRedirect(): Promise { - return + async handleIncomingRedirect (): Promise { + } - async handleRedirectFromLogin(): Promise { - return + async handleRedirectFromLogin (): Promise { + } - async restore(): Promise { - return + async restore (): Promise { + } - async login(): Promise { - return + async login (): Promise { + } - async logout(): Promise { + async logout (): Promise { this.info = { isLoggedIn: false } this.webId = undefined this.isActive = false } - fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + fetch (input: RequestInfo | URL, init?: RequestInit): Promise { return globalThis.fetch(input, init) } - authFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + authFetch (input: RequestInfo | URL, init?: RequestInit): Promise { return globalThis.fetch(input, init) } -} \ No newline at end of file +} From 6aa3a2bc83c33339d10d42fda0bdc6ea9ace2461 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 29 May 2026 15:43:47 +0200 Subject: [PATCH 6/8] conditional solid-logic mapping to sibling checkout when present, otherwise node_modules, so CI and local both work. --- jest.config.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/jest.config.mjs b/jest.config.mjs index a9873594..ba4e70eb 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -1,3 +1,14 @@ +import { existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const localSolidLogicIndex = path.resolve(__dirname, '../solid-logic/src/index.ts') +const solidLogicMapper = existsSync(localSolidLogicIndex) + ? localSolidLogicIndex + : '/node_modules/solid-logic/src/index.ts' + export default { collectCoverage: true, coverageDirectory: 'coverage', @@ -20,8 +31,8 @@ export default { '\\.(svg)$': '/test/__mocks__/fileMock.js', '\\.(png|jpe?g|gif|webp|avif)$': '/test/__mocks__/fileMock.js', '\\.css$': '/test/__mocks__/styleMock.js', - 'solid-logic': '/../solid-logic/src/index.ts', - 'solid-oidc-client-browser': '/test/mocks/solid-oidc-client-browser.ts' + '^solid-logic$': solidLogicMapper, + '^solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts' }, setupFilesAfterEnv: ['./test/helpers/setup.ts'], testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], From cedca6036d943f2b963115b2d1da1fb31f10ee69 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 29 May 2026 15:59:23 +0200 Subject: [PATCH 7/8] fix missing solid-logic --- jest.config.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jest.config.mjs b/jest.config.mjs index ba4e70eb..a86d78c0 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -5,9 +5,7 @@ import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const localSolidLogicIndex = path.resolve(__dirname, '../solid-logic/src/index.ts') -const solidLogicMapper = existsSync(localSolidLogicIndex) - ? localSolidLogicIndex - : '/node_modules/solid-logic/src/index.ts' +const useLocalSolidLogic = existsSync(localSolidLogicIndex) export default { collectCoverage: true, @@ -31,7 +29,7 @@ export default { '\\.(svg)$': '/test/__mocks__/fileMock.js', '\\.(png|jpe?g|gif|webp|avif)$': '/test/__mocks__/fileMock.js', '\\.css$': '/test/__mocks__/styleMock.js', - '^solid-logic$': solidLogicMapper, + ...(useLocalSolidLogic ? { '^solid-logic$': localSolidLogicIndex } : {}), '^solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts' }, setupFilesAfterEnv: ['./test/helpers/setup.ts'], From 8ee589e371974b71e4a01a5ff27ae75f86a5ddd1 Mon Sep 17 00:00:00 2001 From: bourgeoa Date: Fri, 29 May 2026 16:17:35 +0200 Subject: [PATCH 8/8] tests: map both scoped imports to the local mock --- jest.config.mjs | 2 ++ test/mocks/solid-oidc-client-browser.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jest.config.mjs b/jest.config.mjs index a86d78c0..5e114e41 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -30,6 +30,8 @@ export default { '\\.(png|jpe?g|gif|webp|avif)$': '/test/__mocks__/fileMock.js', '\\.css$': '/test/__mocks__/styleMock.js', ...(useLocalSolidLogic ? { '^solid-logic$': localSolidLogicIndex } : {}), + '^@uvdsl/solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts', + '^@uvdsl/solid-oidc-client-browser/core$': '/test/mocks/solid-oidc-client-browser.ts', '^solid-oidc-client-browser$': '/test/mocks/solid-oidc-client-browser.ts' }, setupFilesAfterEnv: ['./test/helpers/setup.ts'], diff --git a/test/mocks/solid-oidc-client-browser.ts b/test/mocks/solid-oidc-client-browser.ts index 009b061b..7e883922 100644 --- a/test/mocks/solid-oidc-client-browser.ts +++ b/test/mocks/solid-oidc-client-browser.ts @@ -54,4 +54,6 @@ export class Session { authFetch (input: RequestInfo | URL, init?: RequestInit): Promise { return globalThis.fetch(input, init) } -} +} + +export class SessionCore extends Session {}