mirror of
https://github.com/asyouplz/SpeechNote.git
synced 2026-07-22 06:43:33 +00:00
* fix(review): resolve obsidian bot validation errors (metadata, lint, ui) * feat(release): prepare version 4.1.0 * fix(ci): add semantic-release configuration * fix(review): address pr feedback and stabilize tests Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(types): resolve typecheck regressions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
325 lines
9.2 KiB
JavaScript
325 lines
9.2 KiB
JavaScript
/**
|
|
* 테스트 환경 설정 및 공통 Mock 설정
|
|
*/
|
|
|
|
const { TextEncoder, TextDecoder } = require('util');
|
|
|
|
// Node.js 환경에서 브라우저 API 모킹
|
|
global.TextEncoder = TextEncoder;
|
|
global.TextDecoder = TextDecoder;
|
|
|
|
jest.setTimeout(15000);
|
|
|
|
const activeTimeouts = new Set();
|
|
const activeIntervals = new Set();
|
|
const originalSetTimeout = global.setTimeout;
|
|
const originalSetInterval = global.setInterval;
|
|
const originalClearTimeout = global.clearTimeout;
|
|
const originalClearInterval = global.clearInterval;
|
|
const originalConsoleWarn = console.warn;
|
|
const originalConsoleError = console.error;
|
|
const originalConsoleInfo = console.info;
|
|
const originalConsoleDebug = console.debug;
|
|
const originalConsoleLog = console.log;
|
|
|
|
const IGNORED_CONSOLE_WARNINGS = ['Workspace event API not available', '[E2E-Test]'];
|
|
const IGNORED_CONSOLE_ERRORS = ['Critical error in render()', '[E2E-Test]'];
|
|
const IGNORED_CONSOLE_INFO = [
|
|
'Whisper provider initialized',
|
|
'Deepgram provider initialized',
|
|
'Providers reinitialized with new configuration',
|
|
];
|
|
const IGNORED_CONSOLE_DEBUG = ['Deepgram provider not initialized: disabled or missing API key'];
|
|
const IGNORED_CONSOLE_LOG = ['[DEBUG] Test message'];
|
|
|
|
const shouldIgnoreConsoleMessage = (ignored, args) =>
|
|
args.some(
|
|
(arg) => typeof arg === 'string' && ignored.some((text) => arg.includes(text))
|
|
);
|
|
|
|
global.setTimeout = (...args) => {
|
|
const id = originalSetTimeout(...args);
|
|
activeTimeouts.add(id);
|
|
return id;
|
|
};
|
|
|
|
global.setInterval = (...args) => {
|
|
const id = originalSetInterval(...args);
|
|
activeIntervals.add(id);
|
|
return id;
|
|
};
|
|
|
|
global.clearTimeout = (id) => {
|
|
activeTimeouts.delete(id);
|
|
return originalClearTimeout(id);
|
|
};
|
|
|
|
global.clearInterval = (id) => {
|
|
activeIntervals.delete(id);
|
|
return originalClearInterval(id);
|
|
};
|
|
|
|
console.warn = (...args) => {
|
|
if (shouldIgnoreConsoleMessage(IGNORED_CONSOLE_WARNINGS, args)) {
|
|
return;
|
|
}
|
|
originalConsoleWarn(...args);
|
|
};
|
|
console.error = (...args) => {
|
|
if (shouldIgnoreConsoleMessage(IGNORED_CONSOLE_ERRORS, args)) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
};
|
|
console.info = (...args) => {
|
|
if (shouldIgnoreConsoleMessage(IGNORED_CONSOLE_INFO, args)) {
|
|
return;
|
|
}
|
|
originalConsoleInfo(...args);
|
|
};
|
|
console.debug = (...args) => {
|
|
if (shouldIgnoreConsoleMessage(IGNORED_CONSOLE_DEBUG, args)) {
|
|
return;
|
|
}
|
|
originalConsoleDebug(...args);
|
|
};
|
|
console.log = (...args) => {
|
|
if (shouldIgnoreConsoleMessage(IGNORED_CONSOLE_LOG, args)) {
|
|
return;
|
|
}
|
|
originalConsoleLog(...args);
|
|
};
|
|
|
|
if (typeof globalThis.createEl !== 'function') {
|
|
globalThis.createEl = (tag, options = {}) => {
|
|
const el = document.createElement(tag);
|
|
const cls = options.cls;
|
|
if (cls) {
|
|
if (Array.isArray(cls)) {
|
|
el.classList.add(...cls.filter(Boolean));
|
|
} else if (typeof cls === 'string') {
|
|
el.className = cls;
|
|
}
|
|
}
|
|
if (options.text !== undefined && options.text !== null) {
|
|
el.textContent = String(options.text);
|
|
}
|
|
if (options.attr) {
|
|
Object.entries(options.attr).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null) {
|
|
el.setAttribute(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
return el;
|
|
};
|
|
}
|
|
|
|
// AudioContext 모킹
|
|
global.AudioContext = jest.fn().mockImplementation(() => ({
|
|
decodeAudioData: jest.fn().mockResolvedValue({
|
|
duration: 10,
|
|
sampleRate: 44100,
|
|
numberOfChannels: 2,
|
|
length: 441000,
|
|
getChannelData: jest.fn().mockReturnValue(new Float32Array(441000)),
|
|
}),
|
|
createBufferSource: jest.fn().mockReturnValue({
|
|
buffer: null,
|
|
connect: jest.fn(),
|
|
start: jest.fn(),
|
|
stop: jest.fn(),
|
|
}),
|
|
createChannelMerger: jest.fn().mockReturnValue({
|
|
connect: jest.fn(),
|
|
}),
|
|
destination: {},
|
|
close: jest.fn(),
|
|
}));
|
|
|
|
// OfflineAudioContext 모킹
|
|
global.OfflineAudioContext = jest.fn().mockImplementation((channels, length, sampleRate) => ({
|
|
length,
|
|
sampleRate,
|
|
numberOfChannels: channels,
|
|
createBufferSource: jest.fn().mockReturnValue({
|
|
buffer: null,
|
|
connect: jest.fn(),
|
|
start: jest.fn(),
|
|
}),
|
|
createChannelMerger: jest.fn().mockReturnValue({
|
|
connect: jest.fn(),
|
|
}),
|
|
destination: {},
|
|
startRendering: jest.fn().mockResolvedValue({
|
|
duration: length / sampleRate,
|
|
sampleRate,
|
|
numberOfChannels: channels,
|
|
length,
|
|
getChannelData: jest.fn().mockReturnValue(new Float32Array(length)),
|
|
}),
|
|
}));
|
|
|
|
// FormData 모킹
|
|
global.FormData = jest.fn().mockImplementation(() => ({
|
|
append: jest.fn(),
|
|
delete: jest.fn(),
|
|
get: jest.fn(),
|
|
getAll: jest.fn(),
|
|
has: jest.fn(),
|
|
set: jest.fn(),
|
|
entries: jest.fn(),
|
|
keys: jest.fn(),
|
|
values: jest.fn(),
|
|
forEach: jest.fn(),
|
|
}));
|
|
|
|
// Blob 모킹
|
|
global.Blob = jest.fn().mockImplementation(function (parts, options) {
|
|
const size = (parts || []).reduce((acc, part) => {
|
|
if (part instanceof ArrayBuffer) return acc + part.byteLength;
|
|
if (typeof part === 'string') return acc + part.length;
|
|
return acc;
|
|
}, 0);
|
|
|
|
return {
|
|
size,
|
|
type: options?.type || '',
|
|
slice: jest.fn(),
|
|
stream: jest.fn(),
|
|
text: jest.fn(),
|
|
arrayBuffer: jest.fn(),
|
|
};
|
|
});
|
|
|
|
// File 모킹
|
|
global.File = jest.fn().mockImplementation(function (parts, name, options) {
|
|
return {
|
|
...new Blob(parts, options),
|
|
name,
|
|
lastModified: options?.lastModified || Date.now(),
|
|
webkitRelativePath: '',
|
|
};
|
|
});
|
|
|
|
// DataTransfer 모킹
|
|
global.DataTransfer = jest.fn().mockImplementation(function () {
|
|
const files = [];
|
|
const items = {
|
|
add: (file) => {
|
|
files.push(file);
|
|
},
|
|
};
|
|
return {
|
|
files,
|
|
items,
|
|
};
|
|
});
|
|
|
|
// DragEvent 모킹
|
|
global.DragEvent = class DragEvent extends Event {
|
|
constructor(type, init = {}) {
|
|
super(type, init);
|
|
this.dataTransfer = init.dataTransfer;
|
|
}
|
|
};
|
|
|
|
// AbortController 모킹
|
|
global.AbortController = jest.fn().mockImplementation(() => ({
|
|
signal: {
|
|
aborted: false,
|
|
onabort: null,
|
|
addEventListener: jest.fn(),
|
|
removeEventListener: jest.fn(),
|
|
},
|
|
abort: jest.fn(function () {
|
|
this.signal.aborted = true;
|
|
if (this.signal.onabort) this.signal.onabort();
|
|
}),
|
|
}));
|
|
|
|
// 커스텀 매처 추가
|
|
expect.extend({
|
|
toBeValidArrayBuffer(received) {
|
|
const pass = received instanceof ArrayBuffer && received.byteLength > 0;
|
|
|
|
if (pass) {
|
|
return {
|
|
message: () => `expected ${received} not to be a valid ArrayBuffer`,
|
|
pass: true,
|
|
};
|
|
} else {
|
|
return {
|
|
message: () => `expected ${received} to be a valid ArrayBuffer with byteLength > 0`,
|
|
pass: false,
|
|
};
|
|
}
|
|
},
|
|
|
|
toContainTimestamp(received) {
|
|
const timestampRegex = /\[\d{2}:\d{2}(:\d{2})?\]/;
|
|
const pass = timestampRegex.test(received);
|
|
|
|
if (pass) {
|
|
return {
|
|
message: () => `expected "${received}" not to contain timestamp`,
|
|
pass: true,
|
|
};
|
|
} else {
|
|
return {
|
|
message: () =>
|
|
`expected "${received}" to contain timestamp in format [MM:SS] or [HH:MM:SS]`,
|
|
pass: false,
|
|
};
|
|
}
|
|
},
|
|
});
|
|
|
|
globalThis.__JEST_FAKE_TIME = Date.now();
|
|
|
|
if (typeof jest !== 'undefined' && typeof jest.advanceTimersByTime === 'function') {
|
|
const originalAdvanceTimersByTime = jest.advanceTimersByTime.bind(jest);
|
|
jest.advanceTimersByTime = (...args) => {
|
|
try {
|
|
return originalAdvanceTimersByTime(...args);
|
|
} catch (error) {
|
|
if (
|
|
error &&
|
|
typeof error.message === 'string' &&
|
|
error.message.includes('not replaced with fake timers')
|
|
) {
|
|
// continue to update mock time for compatibility
|
|
} else {
|
|
throw error;
|
|
}
|
|
} finally {
|
|
const increment = typeof args[0] === 'number' ? args[0] : 0;
|
|
if (!Number.isNaN(increment)) {
|
|
if (process.env.DEBUG_NOTIFICATION) {
|
|
console.log('Advance timers by', increment);
|
|
}
|
|
if (typeof globalThis.__JEST_FAKE_TIME !== 'number') {
|
|
globalThis.__JEST_FAKE_TIME = Date.now();
|
|
}
|
|
globalThis.__JEST_FAKE_TIME += increment;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
activeTimeouts.forEach((id) => originalClearTimeout(id));
|
|
activeTimeouts.clear();
|
|
activeIntervals.forEach((id) => originalClearInterval(id));
|
|
activeIntervals.clear();
|
|
|
|
globalThis.__JEST_FAKE_TIME = Date.now();
|
|
try {
|
|
const { requestUrl } = require('obsidian');
|
|
if (typeof requestUrl?.mockReset === 'function') {
|
|
requestUrl.mockReset();
|
|
}
|
|
} catch (error) {
|
|
// ignore in case module is not available
|
|
}
|
|
});
|