mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 01:54:53 -04:00
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2
- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
`it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
instead of relying on `@types/jest` ambient declarations. Under the
new tsgo build, `import { jest } from '@jest/globals'` shadows the
ambient `jest` namespace, breaking `@types/jest`'s `declare var
describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
now import from it, and add `@types/node` to packages that need it
but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
no longer globally available.
* chore: fix remaining tsgo type-strictness errors
- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
toStrictEqual / toEqual; @jest/globals rejects the typed objects
(which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.
* chore: address Copilot review on #11332
- Move misplaced `@jest/globals` imports to the top import block in
checkEngine, run.ts, and workspace/root-finder tests where the
script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
in bins/linker, lockfile/fs, and resolving/local-resolver tests with
`await expect(x()).rejects.toMatchObject({...})`. The old pattern
swallowed an unrelated `throw` if the under-test call silently
succeeded, which would fail on the catch-block assertion with a
misleading message.
263 lines
7.6 KiB
TypeScript
263 lines
7.6 KiB
TypeScript
import { beforeEach, describe, expect, it, jest } from '@jest/globals'
|
|
import type {
|
|
PromptBrowserOpenContext,
|
|
PromptBrowserOpenReadlineInterface,
|
|
} from '@pnpm/network.web-auth'
|
|
|
|
const mockOpen = jest.fn<(target: string) => Promise<unknown>>()
|
|
jest.unstable_mockModule('open', () => ({
|
|
default: mockOpen,
|
|
}))
|
|
|
|
const { promptBrowserOpen } = await import('@pnpm/network.web-auth')
|
|
|
|
function createDeferred<T> (): {
|
|
promise: Promise<T>
|
|
resolve: (value: T) => void
|
|
reject: (reason?: unknown) => void
|
|
} {
|
|
let resolve!: (value: T) => void
|
|
let reject!: (reason?: unknown) => void
|
|
const promise = new Promise<T>((res, rej) => {
|
|
resolve = res
|
|
reject = rej
|
|
})
|
|
return { promise, resolve, reject }
|
|
}
|
|
|
|
interface MockReadlineInterface extends PromptBrowserOpenReadlineInterface {
|
|
simulateEnterKeypress: () => void
|
|
}
|
|
|
|
const createMockReadlineInterface = (): MockReadlineInterface => {
|
|
let lineListener: (() => void) | undefined
|
|
return {
|
|
once: (_event: string, listener: () => void) => {
|
|
lineListener = listener
|
|
},
|
|
close: jest.fn<() => void>(),
|
|
simulateEnterKeypress: () => lineListener?.(),
|
|
}
|
|
}
|
|
|
|
type MockContextOverrides = Omit<Partial<PromptBrowserOpenContext>, 'process'> & {
|
|
process?: Partial<PromptBrowserOpenContext['process']>
|
|
}
|
|
|
|
const createMockContext = (overrides?: MockContextOverrides): PromptBrowserOpenContext => ({
|
|
globalInfo: () => {},
|
|
globalWarn: () => {},
|
|
...overrides,
|
|
process: {
|
|
stdin: { isTTY: true },
|
|
...overrides?.process,
|
|
},
|
|
})
|
|
|
|
beforeEach(() => {
|
|
mockOpen.mockReset()
|
|
mockOpen.mockResolvedValue(undefined)
|
|
})
|
|
|
|
describe('promptBrowserOpen', () => {
|
|
it('returns the poll result when poll completes before Enter keypress', async () => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
})
|
|
|
|
const token = await promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: Promise.resolve('my-token'),
|
|
})
|
|
|
|
expect(token).toBe('my-token')
|
|
expect(mockRl.close).toHaveBeenCalled()
|
|
expect(mockOpen).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('opens browser via open package when Enter key is pressed before poll completes', async () => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const pollDeferred = createDeferred<string>()
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
})
|
|
|
|
const resultPromise = promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: pollDeferred.promise,
|
|
})
|
|
|
|
mockRl.simulateEnterKeypress()
|
|
|
|
await new Promise<void>(resolve => queueMicrotask(resolve))
|
|
|
|
expect(mockOpen).toHaveBeenCalledWith('https://example.com/auth')
|
|
|
|
pollDeferred.resolve('token-after-enter')
|
|
const token = await resultPromise
|
|
|
|
expect(token).toBe('token-after-enter')
|
|
expect(mockRl.close).toHaveBeenCalled()
|
|
})
|
|
|
|
it('warns and continues polling when open fails', async () => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const pollDeferred = createDeferred<string>()
|
|
const globalWarn = jest.fn<(msg: string) => void>()
|
|
const globalInfo = jest.fn<(msg: string) => void>()
|
|
mockOpen.mockRejectedValue(new Error('xdg-open not found'))
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
globalInfo,
|
|
globalWarn,
|
|
})
|
|
|
|
const resultPromise = promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: pollDeferred.promise,
|
|
})
|
|
|
|
mockRl.simulateEnterKeypress()
|
|
|
|
await new Promise<void>(resolve => queueMicrotask(resolve))
|
|
await new Promise<void>(resolve => queueMicrotask(resolve))
|
|
|
|
expect(globalWarn).toHaveBeenCalledWith(expect.stringContaining('xdg-open not found'))
|
|
expect(globalInfo).toHaveBeenCalledWith('Please open the URL shown above manually.')
|
|
|
|
pollDeferred.resolve('tok')
|
|
expect(await resultPromise).toBe('tok')
|
|
})
|
|
|
|
it('warns and falls back to plain poll when createReadlineInterface throws', async () => {
|
|
const globalWarn = jest.fn<(msg: string) => void>()
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => {
|
|
throw new Error('setRawMode not supported')
|
|
},
|
|
globalWarn,
|
|
})
|
|
|
|
const token = await promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: Promise.resolve('fallback-token'),
|
|
})
|
|
|
|
expect(token).toBe('fallback-token')
|
|
expect(globalWarn).toHaveBeenCalledWith(expect.stringContaining('setRawMode not supported'))
|
|
expect(mockOpen).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('falls back to plain poll when createReadlineInterface is not provided', async () => {
|
|
const context = createMockContext()
|
|
|
|
const token = await promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: Promise.resolve('plain-token'),
|
|
})
|
|
|
|
expect(token).toBe('plain-token')
|
|
})
|
|
|
|
it('falls back to plain poll when stdin is not a TTY', async () => {
|
|
const context = createMockContext({
|
|
createReadlineInterface: createMockReadlineInterface,
|
|
process: { stdin: { isTTY: false } },
|
|
})
|
|
|
|
const token = await promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: Promise.resolve('plain-token'),
|
|
})
|
|
|
|
expect(token).toBe('plain-token')
|
|
})
|
|
|
|
it('shows the press-Enter message', async () => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const globalInfo = jest.fn<(msg: string) => void>()
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
globalInfo,
|
|
})
|
|
|
|
await promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: Promise.resolve('tok'),
|
|
})
|
|
|
|
expect(globalInfo).toHaveBeenCalledWith('Press ENTER to open the URL in your browser.')
|
|
})
|
|
|
|
it.each([
|
|
['javascript:alert(1)'],
|
|
['file:///etc/passwd'],
|
|
['not a url'],
|
|
])('does not open browser for non-http(s) authUrl %s', async (authUrl) => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const pollDeferred = createDeferred<string>()
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
})
|
|
|
|
const resultPromise = promptBrowserOpen({
|
|
authUrl,
|
|
context,
|
|
pollPromise: pollDeferred.promise,
|
|
})
|
|
|
|
pollDeferred.resolve('tok')
|
|
expect(await resultPromise).toBe('tok')
|
|
expect(mockOpen).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('continues polling when open throws synchronously', async () => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const pollDeferred = createDeferred<string>()
|
|
const globalWarn = jest.fn<(msg: string) => void>()
|
|
mockOpen.mockImplementation(() => {
|
|
throw new Error('sync failure')
|
|
})
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
globalWarn,
|
|
})
|
|
|
|
const resultPromise = promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: pollDeferred.promise,
|
|
})
|
|
|
|
mockRl.simulateEnterKeypress()
|
|
|
|
expect(globalWarn).toHaveBeenCalledWith(expect.stringContaining('sync failure'))
|
|
|
|
pollDeferred.resolve('tok')
|
|
expect(await resultPromise).toBe('tok')
|
|
})
|
|
|
|
it('cleans up when poll rejects', async () => {
|
|
const mockRl = createMockReadlineInterface()
|
|
const context = createMockContext({
|
|
createReadlineInterface: () => mockRl,
|
|
})
|
|
|
|
await expect(promptBrowserOpen({
|
|
authUrl: 'https://example.com/auth',
|
|
context,
|
|
pollPromise: Promise.reject(new Error('timeout')),
|
|
})).rejects.toThrow('timeout')
|
|
|
|
expect(mockRl.close).toHaveBeenCalled()
|
|
})
|
|
})
|