{
+ toContainGraph(expected: IndexedFormula): R
+ toEqualGraph(expected: IndexedFormula): R
+ toBeCalled(): R
+ toBeCalledWith(...args: any[]): R
+ toReturn(): R
+ toThrowError(error?: any): R
+ }
+ }
+}
+
+export {}
diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts
index 9fc8edb84..5dec4cafc 100644
--- a/test/helpers/setup.ts
+++ b/test/helpers/setup.ts
@@ -2,18 +2,30 @@ import { toContainGraph } from '../custom-matchers/toContainGraph'
import { toEqualGraph } from '../custom-matchers/toEqualGraph'
import 'isomorphic-fetch'
import { TextEncoder, TextDecoder } from 'util'
+import { TransformStream, ReadableStream, WritableStream } from 'stream/web'
// https://stackoverflow.com/questions/52612122/how-to-use-jest-to-test-functions-using-crypto-or-window-mscrypto
import crypto from 'crypto'
-global.crypto = {
- getRandomValues: function (buffer) {
- return crypto.randomFillSync(buffer)
- }
-}
+Object.defineProperty(globalThis, 'crypto', {
+ value: crypto.webcrypto,
+ configurable: true
+})
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
+global.TransformStream = TransformStream as unknown as typeof globalThis.TransformStream
+global.ReadableStream = ReadableStream as unknown as typeof globalThis.ReadableStream
+global.WritableStream = WritableStream as unknown as typeof globalThis.WritableStream
+
+// Node provides MessagePort via worker_threads; jsdom/undici expects it in global scope
+try {
+ const { MessageChannel, MessagePort } = require('worker_threads')
+ global.MessageChannel = MessageChannel
+ global.MessagePort = MessagePort
+} catch (err) {
+ // worker_threads not available (older Node), ignore
+}
// Mock external dependencies that solid-logic expects
jest.mock('$rdf', () => require('rdflib'), { virtual: true })
diff --git a/test/unit/__snapshots__/tabs.test.ts.snap b/test/unit/__snapshots__/tabs.test.ts.snap
index f37782410..a71a0a7f3 100644
--- a/test/unit/__snapshots__/tabs.test.ts.snap
+++ b/test/unit/__snapshots__/tabs.test.ts.snap
@@ -1,18 +1,18 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`tabWidget minimal setup of options renders content for first tab 1`] = `
-
-
-
+
`;
diff --git a/test/unit/acl/acl.test.ts b/test/unit/acl/acl.test.ts
index 7d1baeab8..be3ff04ca 100644
--- a/test/unit/acl/acl.test.ts
+++ b/test/unit/acl/acl.test.ts
@@ -156,10 +156,10 @@ describe('getACL', () => {
case DEFAULT_RESOURCE_DOC:
solidLogicSingleton.store.add(DEFAULT_RESOURCE_DOC, ACL_LINK, DEFAULT_RESOURCE_ACL, DEFAULT_RESOURCE_DOC)
// eslint-disable-next-line n/no-callback-literal
- return callback(true)
+ return callback?.(true, '')
case DEFAULT_RESOURCE_ACL:
// eslint-disable-next-line n/no-callback-literal
- return callback(true)
+ return callback?.(true, '')
}
}
getACL(DEFAULT_RESOURCE_DOC, callbackFunction)
@@ -174,7 +174,7 @@ describe('getACL', () => {
solidLogicSingleton.store.add(DEFAULT_RESOURCE_DOC, ACL_LINK, DEFAULT_RESOURCE_ACL, DEFAULT_RESOURCE_DOC)
solidLogicSingleton.store.fetcher.nonexistent[DEFAULT_RESOURCE_ACL.uri] = true
// eslint-disable-next-line n/no-callback-literal
- return callback(true)
+ return callback?.(true, '')
}
getACL(DEFAULT_RESOURCE_DOC, callbackFunction)
})
@@ -191,10 +191,10 @@ describe('getACL', () => {
case DEFAULT_RESOURCE_DOC:
solidLogicSingleton.store.add(DEFAULT_RESOURCE_DOC, ACL_LINK, DEFAULT_RESOURCE_ACL, DEFAULT_RESOURCE_DOC)
// eslint-disable-next-line n/no-callback-literal
- return callback(true)
+ return callback?.(true, '')
case DEFAULT_RESOURCE_ACL:
// eslint-disable-next-line n/no-callback-literal
- return callback(false, errorMessage, { status: 500 })
+ return callback?.(false, errorMessage, { status: 500 } as Response)
}
}
getACL(DEFAULT_RESOURCE_DOC, callbackFunction)
@@ -207,7 +207,7 @@ describe('getACL', () => {
describe('no ACL link for resource', () => {
beforeEach(() => {
// eslint-disable-next-line n/no-callback-literal
- solidLogicSingleton.store.fetcher.nowOrWhenFetched = (doc, options, callback) => callback(true)
+ solidLogicSingleton.store.fetcher.nowOrWhenFetched = (doc, options, callback) => callback?.(true, '')
getACL(DEFAULT_RESOURCE_DOC, callbackFunction)
})
@@ -217,7 +217,7 @@ describe('getACL', () => {
describe('failed response for doc', () => {
beforeEach(() => {
// eslint-disable-next-line n/no-callback-literal
- solidLogicSingleton.store.fetcher.nowOrWhenFetched = (doc, options, callback) => callback(false, 'Failed response')
+ solidLogicSingleton.store.fetcher.nowOrWhenFetched = (doc, options, callback) => callback?.(false, 'Failed response')
getACL(DEFAULT_RESOURCE_DOC, callbackFunction)
})
diff --git a/test/unit/login/login.test.ts b/test/unit/login/login.test.ts
index 455c99de2..0b67f3e67 100644
--- a/test/unit/login/login.test.ts
+++ b/test/unit/login/login.test.ts
@@ -11,3 +11,33 @@ describe('ensureLoggedIn', () => {
expect(testLogin.ensureLoggedIn({})).toBeInstanceOf(Object)
})
})
+
+describe('getUserRoles', () => {
+ afterEach(() => {
+ jest.restoreAllMocks()
+ jest.resetModules()
+ })
+
+ it('returns [] and does not load preferences when current user is missing', async () => {
+ const solidLogic = require('solid-logic')
+
+ // Note: `authSession.info` is a derived read-only property (from
+ // webId/isActive) and can no longer be assigned. The logged-out state is
+ // driven by mocking `currentUser` to return null below.
+
+ const currentUserSpy = jest
+ .spyOn(solidLogic.authn, 'currentUser')
+ .mockReturnValue(null)
+ const loadPreferencesSpy = jest.spyOn(
+ solidLogic.solidLogicSingleton.profile,
+ 'loadPreferences'
+ )
+
+ const loginModule = require('../../../src/login/login')
+ const roles = await loginModule.getUserRoles()
+
+ expect(currentUserSpy).toHaveBeenCalled()
+ expect(roles).toEqual([])
+ expect(loadPreferencesSpy).not.toHaveBeenCalled()
+ })
+})
diff --git a/test/unit/pad.test.ts b/test/unit/pad.test.ts
index 71f30a2db..807b9e8ec 100644
--- a/test/unit/pad.test.ts
+++ b/test/unit/pad.test.ts
@@ -3,6 +3,8 @@ import { JSDOM } from 'jsdom'
import * as RdfLib from 'rdflib'
import { lightColorHash, notepad } from '../../src/pad'
import { log } from '../../src/debug'
+import ns from '../../src/ns'
+import { solidLogicSingleton } from 'solid-logic'
silenceDebugMessages()
const window = new JSDOM('Hello world
').window
@@ -25,6 +27,39 @@ describe('lightColorHash', () => {
})
describe('notepad', () => {
+ const store: any = solidLogicSingleton.store
+ const PAD = RdfLib.Namespace('http://www.w3.org/ns/pim/pad#')
+ let originalUpdater: any
+
+ function setupExistingPadFixture () {
+ const id = Date.now().toString() + Math.random().toString().slice(2)
+ const padDoc = new RdfLib.NamedNode(`https://pad.example/${id}.ttl`)
+ const subject = new RdfLib.NamedNode(`https://pad.example/${id}.ttl#pad`)
+ const chunk = new RdfLib.NamedNode(`https://pad.example/${id}.ttl#line1`)
+ const me = new RdfLib.NamedNode('https://sharonstrats.inrupt.net/profile/card#me')
+
+ store.add(subject, PAD('next'), chunk, padDoc)
+ store.add(chunk, PAD('next'), subject, padDoc)
+ store.add(chunk, ns.sioc('content'), 'initial', padDoc)
+ store.add(chunk, ns.dc('author'), me, padDoc)
+
+ const table = notepad(dom, padDoc, subject, me, { exists: true })
+ const part = table.querySelector('input') as any
+ if (!part) {
+ throw new Error('Expected notepad to render an input part')
+ }
+ return { padDoc, subject, chunk, me, part }
+ }
+
+ beforeEach(() => {
+ originalUpdater = store.updater
+ })
+
+ afterEach(() => {
+ store.updater = originalUpdater
+ jest.useRealTimers()
+ })
+
it('to be exposed by the Public API', () => {
expect(notepad).toBe(notepad)
})
@@ -75,4 +110,84 @@ describe('notepad', () => {
expect(notepad(dom, padDoc, subject, me, options)
).resolves.toBe({})
})
+
+ it('debounces rapid input and sends one update after pause', () => {
+ jest.useFakeTimers()
+
+ const update = jest.fn((_del, _ins, cb) => cb(null, true, '', { status: 200 }))
+ store.updater = {
+ update,
+ requestDownstreamAction: jest.fn(),
+ reload: jest.fn(),
+ store
+ }
+
+ const { padDoc, subject, chunk, part } = setupExistingPadFixture()
+
+ expect(() => {
+ part.value = 'a'
+ part.dispatchEvent(new window.Event('input', { bubbles: true }))
+ part.value = 'ab'
+ part.dispatchEvent(new window.Event('input', { bubbles: true }))
+ part.value = 'abc'
+ part.dispatchEvent(new window.Event('input', { bubbles: true }))
+ }).not.toThrow()
+
+ expect(update).toHaveBeenCalledTimes(0)
+ jest.advanceTimersByTime(399)
+ expect(update).toHaveBeenCalledTimes(0)
+ jest.advanceTimersByTime(1)
+ expect(update).toHaveBeenCalledTimes(1)
+
+ // Cleanup this test fixture's statements.
+ store.removeMatches(subject, null, null, padDoc)
+ store.removeMatches(chunk, null, null, padDoc)
+ store.removeMatches(null, null, chunk, padDoc)
+ })
+
+ it('retries on transient 503 and keeps state/lastSent coherent', () => {
+ jest.useFakeTimers()
+
+ let callCount = 0
+ const update = jest.fn((_del, _ins, cb) => {
+ callCount += 1
+ if (callCount === 1) {
+ cb(null, false, 'transient', { status: 503 })
+ } else {
+ cb(null, true, '', { status: 200 })
+ }
+ })
+
+ store.updater = {
+ update,
+ requestDownstreamAction: jest.fn(),
+ reload: jest.fn(),
+ store
+ }
+
+ const { padDoc, subject, chunk, part } = setupExistingPadFixture()
+ part.value = 'queued text'
+
+ expect(() => {
+ part.dispatchEvent(new window.Event('input', { bubbles: true }))
+ jest.advanceTimersByTime(400) // debounce fires first PATCH
+ }).not.toThrow()
+
+ expect(update).toHaveBeenCalledTimes(1)
+ expect(part.state).toBe(0)
+ expect(part.lastSent).toBeUndefined()
+
+ jest.advanceTimersByTime(1999)
+ expect(update).toHaveBeenCalledTimes(1)
+ jest.advanceTimersByTime(1)
+
+ expect(update).toHaveBeenCalledTimes(2)
+ expect(part.state).toBe(0)
+ expect(part.lastSent).toBe('queued text')
+
+ // Cleanup this test fixture's statements.
+ store.removeMatches(subject, null, null, padDoc)
+ store.removeMatches(chunk, null, null, padDoc)
+ store.removeMatches(null, null, chunk, padDoc)
+ })
})
diff --git a/test/unit/tabs.test.ts b/test/unit/tabs.test.ts
index a30a350dd..c8da5d899 100644
--- a/test/unit/tabs.test.ts
+++ b/test/unit/tabs.test.ts
@@ -76,7 +76,7 @@ describe('tabWidget', () => {
describe('bodyContainer', () => {
it('adds property bodyContainer', () => {
- expect(tabWidgetElement.bodyContainer).toBe(tabWidgetElement.querySelector('main'))
+ expect(tabWidgetElement.bodyContainer).toBe(tabWidgetElement.querySelector('nav + div'))
})
})
@@ -123,7 +123,7 @@ describe('tabWidget', () => {
tabWidgetElement = tabs.tabWidget({ backgroundColor: '#ff0000', ...minimalOptions })
expect(tabWidgetElement.tabContainer.querySelector('[style]').style['background-color']).toEqual('rgb(178, 0, 0)')
expect(tabWidgetElement.tabContainer.querySelector('[style]').style.color).toEqual('rgb(255, 255, 255)')
- expect(tabWidgetElement.bodyContainer.querySelector('main').style['border-color']).toEqual('rgb(178, 0, 0)')
+ expect((tabWidgetElement.bodyContainer.children[0].children[0] as HTMLElement).style.borderColor).toEqual('rgb(178, 0, 0)')
})
it('considers lighter colors and set color of text accordingly', () => {
diff --git a/test/unit/widgets/__snapshots__/error.test.ts.snap b/test/unit/widgets/__snapshots__/error.test.ts.snap
index 961a26197..3565142d9 100644
--- a/test/unit/widgets/__snapshots__/error.test.ts.snap
+++ b/test/unit/widgets/__snapshots__/error.test.ts.snap
@@ -2,7 +2,7 @@
exports[`errorMessageBlock creates an error message div 1`] = `
my error message