From 6de1de25b7605bcec86795bb3086ca33659adfb9 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 02:47:49 +0100 Subject: [PATCH 01/92] [lib] fix bugs in errors, models, lang, and query utilities - errors.js: compare window.location.pathname instead of window.location object - models.js: check findIndex result against -1 instead of null - lang.js: fix zh_Hant_TW language code typo (zw -> zh) - query.js: encode URI components to prevent broken query strings Co-Authored-By: Claude Opus 4.6 --- src/lib/errors.js | 2 +- src/lib/lang.js | 2 +- src/lib/models.js | 2 +- src/lib/query.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/errors.js b/src/lib/errors.js index 81f65c6e9b..1a777f8328 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -1,6 +1,6 @@ const errors = { backToLogin() { - if (window.location !== '/login') { + if (window.location.pathname !== '/login') { window.location.replace('/login') } } diff --git a/src/lib/lang.js b/src/lib/lang.js index 075037523e..872ebca7df 100644 --- a/src/lib/lang.js +++ b/src/lib/lang.js @@ -3,7 +3,7 @@ import i18n from '@/lib/i18n' const LOCALE_MAP = { zh_Hans_CN: { language: 'zh', localeCode: 'zh-cn' }, - zh_Hant_TW: { language: 'zw', localeCode: 'zh-tw' } + zh_Hant_TW: { language: 'zh', localeCode: 'zh-tw' } } export default { diff --git a/src/lib/models.js b/src/lib/models.js index 2254b93bb7..d62ac4ec2a 100644 --- a/src/lib/models.js +++ b/src/lib/models.js @@ -95,7 +95,7 @@ export const addToIdList = (production, field, id) => { export const removeFromIdList = (production, field, id) => { const index = production[field].findIndex(mid => mid === id) - if (index !== null) production[field].splice(index, 1) + if (index !== -1) production[field].splice(index, 1) } export const arrayMove = (array, fromIndex, toIndex) => { diff --git a/src/lib/query.js b/src/lib/query.js index c9a5764e55..8b8ef1a5a6 100644 --- a/src/lib/query.js +++ b/src/lib/query.js @@ -15,7 +15,7 @@ export const buildQueryString = (path, params) => { params[key] || (typeof params[key] === 'boolean' && params[key] === false) ) - couples.push(`${key}=${params[key]}`) + couples.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) }) return result + couples.join('&') } From 54f7396cc7b32d3ab426de6906b07ff220bdd880 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 02:53:36 +0100 Subject: [PATCH 02/92] [lib] improve performance in video, render, indexing, and sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - video.js: extract PRECISION_FACTOR constant and roundPrecision helper, replace deprecated substr with substring - render.js: batch all mention/department/taskType replacements into a single regex pass instead of O(n²) replaceAll loops - indexing.js: use substring prefix instead of character-by-character string concatenation - sorting.js: cache getTaskTypePriorityOfProd results before sorting to avoid redundant calls per comparison Co-Authored-By: Claude Opus 4.6 --- src/lib/indexing.js | 18 +++++++++--------- src/lib/render.js | 27 +++++++++++++++++++------- src/lib/sorting.js | 46 +++++++++++++++++++++++++-------------------- src/lib/video.js | 27 +++++++++++++------------- 4 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/lib/indexing.js b/src/lib/indexing.js index 5088770642..8caac1515f 100644 --- a/src/lib/indexing.js +++ b/src/lib/indexing.js @@ -238,18 +238,18 @@ const indexSearchWord = (index, word) => { */ const indexWords = (index, entryIndex, entry, words) => { for (const word of words) { - let currentString = '' if (word) { - for (const character of word) { - currentString += character.toLowerCase() - if (index[currentString] === undefined) { - index[currentString] = [] - entryIndex[currentString] = Object.create(null) + const lowerWord = word.toLowerCase() + for (let i = 1; i <= lowerWord.length; i++) { + const prefix = lowerWord.substring(0, i) + if (index[prefix] === undefined) { + index[prefix] = [] + entryIndex[prefix] = Object.create(null) } - if (!entryIndex[currentString][entry.id]) { - index[currentString].push(entry) - entryIndex[currentString][entry.id] = true + if (!entryIndex[prefix][entry.id]) { + index[prefix].push(entry) + entryIndex[prefix][entry.id] = true } } } diff --git a/src/lib/render.js b/src/lib/render.js index bb2ed054ad..201743d85e 100644 --- a/src/lib/render.js +++ b/src/lib/render.js @@ -54,11 +54,13 @@ export const renderComment = ( ) => { let html = renderMarkdown(input) + const replacements = new Map() + if (mentions) { for (const personId of mentions) { const person = personMap.get(personId) if (!person) continue - html = html.replaceAll( + replacements.set( `@${person.full_name}`, `@${person.full_name}` ) @@ -66,7 +68,7 @@ export const renderComment = ( for (const departmentId of departmentMentions) { const department = departmentMap.get(departmentId) if (!department) continue - html = html.replaceAll( + replacements.set( `@${department.name}`, `@${department.name}` ) @@ -74,22 +76,33 @@ export const renderComment = ( } if (taskTypes) { - // replace #TaskType with a link to the task within the same entity taskTypes.forEach(taskType => { const task_name = encodeHtmlEntities(taskType.name) - if (taskType.url) - html = html.replaceAll( + if (taskType.url) { + replacements.set( `#${task_name}`, `#${task_name}` ) + } }) - // replace #All with a link to the shot - html = html.replaceAll( + replacements.set( '#All', `#All` ) } + if (replacements.size > 0) { + const escapeRegex = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const pattern = [...replacements.keys()] + .sort((a, b) => b.length - a.length) + .map(escapeRegex) + .join('|') + html = html.replace( + new RegExp(pattern, 'g'), + match => replacements.get(match) + ) + } + return html.replaceAll( TIME_CODE_REGEX, (match, version, hours, minutes, seconds, sep, subframes, frame) => { diff --git a/src/lib/sorting.js b/src/lib/sorting.js index cd5f11794c..2dc3821046 100644 --- a/src/lib/sorting.js +++ b/src/lib/sorting.js @@ -174,19 +174,22 @@ export const sortTaskTypeScheduleItems = ( currentProduction, taskTypeMap ) => { + const priorityCache = new Map() + for (const item of items) { + if (!priorityCache.has(item.task_type_id)) { + const taskType = taskTypeMap.get(item.task_type_id) + priorityCache.set( + item.task_type_id, + getTaskTypePriorityOfProd(taskType, currentProduction) + ) + } + } const sortFunc = firstBy('for_entity') .thenBy((itemA, itemB) => { - const taskTypeA = taskTypeMap.get(itemA.task_type_id) - const taskTypeB = taskTypeMap.get(itemB.task_type_id) - const taskTypeAPriority = getTaskTypePriorityOfProd( - taskTypeA, - currentProduction - ) - const taskTypeBPriority = getTaskTypePriorityOfProd( - taskTypeB, - currentProduction + return ( + priorityCache.get(itemA.task_type_id) - + priorityCache.get(itemB.task_type_id) ) - return taskTypeAPriority - taskTypeBPriority }) .thenBy('name') return items.sort(sortFunc) @@ -233,18 +236,21 @@ export const sortValidationColumns = ( taskTypeMap, currentProduction ) => { + const priorityCache = new Map() + for (const id of columns) { + if (!priorityCache.has(id)) { + priorityCache.set( + id, + getTaskTypePriorityOfProd(taskTypeMap.get(id), currentProduction) + ) + } + } return columns.sort((a, b) => { - const taskTypeA = taskTypeMap.get(a) - const taskTypeB = taskTypeMap.get(b) - const taskTypeAPriority = getTaskTypePriorityOfProd( - taskTypeA, - currentProduction - ) - const taskTypeBPriority = getTaskTypePriorityOfProd( - taskTypeB, - currentProduction - ) + const taskTypeAPriority = priorityCache.get(a) + const taskTypeBPriority = priorityCache.get(b) if (taskTypeAPriority === taskTypeBPriority) { + const taskTypeA = taskTypeMap.get(a) + const taskTypeB = taskTypeMap.get(b) return taskTypeA.name.localeCompare(taskTypeB.name, undefined, { numeric: true }) diff --git a/src/lib/video.js b/src/lib/video.js index c855b3b8c8..ecdd4ecdbb 100644 --- a/src/lib/video.js +++ b/src/lib/video.js @@ -1,28 +1,27 @@ +const PRECISION_FACTOR = 10000 + +const roundPrecision = value => + Math.round(value * PRECISION_FACTOR) / PRECISION_FACTOR + /* * Make sure that given time matches a frame in the video */ export const roundToFrame = (time, fps) => { - const frameFactor = Math.round((1 / fps) * 10000) / 10000 + const frameFactor = roundPrecision(1 / fps) const frameNumber = Math.round(time / frameFactor) - let roundedTime = frameNumber * frameFactor - roundedTime = Math.round(roundedTime * 10000) / 10000 - return roundedTime + return roundPrecision(frameNumber * frameFactor) } export const ceilToFrame = (time, fps) => { - const frameFactor = Math.round((1 / fps) * 10000) / 10000 + const frameFactor = roundPrecision(1 / fps) const frameNumber = Math.ceil(time / frameFactor) - let roundedTime = frameNumber * frameFactor - roundedTime = Math.ceil(roundedTime * 10000) / 10000 - return roundedTime + return Math.ceil(frameNumber * frameFactor * PRECISION_FACTOR) / PRECISION_FACTOR } export const floorToFrame = (time, fps) => { - const frameFactor = Math.round((1 / fps) * 10000) / 10000 + const frameFactor = roundPrecision(1 / fps) const frameNumber = Math.floor(time / frameFactor) - let roundedTime = frameNumber * frameFactor - roundedTime = Math.floor(roundedTime * 10000) / 10000 - return roundedTime + return Math.floor(frameNumber * frameFactor * PRECISION_FACTOR) / PRECISION_FACTOR } /* @@ -43,13 +42,13 @@ export const formatTime = (rawTime, fps) => { const time = new Date(1000 * rawTime).toISOString() const milliseconds = parseInt(time.substring(20, 23)) - const frameDuration = Math.round((1 / fps) * 10000) / 10000 + const frameDuration = roundPrecision(1 / fps) const frame = `${Math.round(milliseconds / (1000 * frameDuration))}`.padStart( 2, '0' ) try { - return `${time.substr(11, 8)}:${frame}` + return `${time.substring(11, 19)}:${frame}` } catch (err) { console.error(err) return '00:00:00:00' From 2c4a60faf65f8488ecc03cb952ecd978f3d6a7ca Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 02:57:19 +0100 Subject: [PATCH 03/92] [lib] reduce major code duplication in csv, time, sorting, and indexing - csv.js: merge getStatReportsEntries and getRetakeStatReportsEntries into one function with options parameter (excludeStatuses, sortEntries) - time.js: extract shared adjustBusinessDays helper for addBusinessDays and removeBusinessDays - sorting.js: consolidate 5 sort*Result functions into a single sortEntityResult helper with per-entity config - indexing.js: extract createIndexPair factory to replace 8 duplicated Object.create(null) pairs Co-Authored-By: Claude Opus 4.6 --- src/lib/csv.js | 102 ++++++++++++------------------------ src/lib/indexing.js | 43 +++++++-------- src/lib/sorting.js | 125 +++++++++++++++----------------------------- src/lib/time.js | 35 ++++--------- 4 files changed, 104 insertions(+), 201 deletions(-) diff --git a/src/lib/csv.js b/src/lib/csv.js index a04b17ff91..826c303b46 100644 --- a/src/lib/csv.js +++ b/src/lib/csv.js @@ -258,7 +258,8 @@ const csv = { taskStatusMap, entryMap, countMode = 'count', - production + production, + { excludeStatuses = [], sortEntries = true } = {} ) { let entries = [] const taskTypeIds = getStatsTaskTypeIds(mainStats, taskTypeMap, production) @@ -266,6 +267,7 @@ const csv = { entryIds.forEach(entryId => { const taskStatusIds = getStatsTaskStatusIdsForEntry(mainStats, entryId) + .filter(s => !excludeStatuses.includes(s)) const total = getStatsTotalCount( mainStats, taskStatusIds, @@ -304,19 +306,21 @@ const csv = { ) } else { Object.keys(mainStats[entryId].all).forEach(taskStatusId => { - lineMap[taskStatusId] = lineMap[taskStatusId].concat(['', '']) + if (!excludeStatuses.includes(taskStatusId)) { + lineMap[taskStatusId] = lineMap[taskStatusId].concat(['', '']) + } }) } } }) - entries = entries.concat( - Object.values(lineMap).sort((a, b) => { - return a[1].localeCompare(b[1], undefined, { - numeric: true - }) - }) - ) + const values = Object.values(lineMap) + if (sortEntries) { + values.sort((a, b) => + a[1].localeCompare(b[1], undefined, { numeric: true }) + ) + } + entries = entries.concat(values) entries.push(['']) }) return entries @@ -336,13 +340,18 @@ const csv = { taskTypeMap, production ) - const entries = csv.getRetakeStatReportsEntries( + const retakeOptions = { + excludeStatuses: ['max_retake_count', 'evolution'], + sortEntries: false + } + const entries = csv.getStatReportsEntries( mainStats, taskTypeMap, taskStatusMap, entryMap, countMode, - production + production, + retakeOptions ) const lines = [headers, ...entries] return csv.buildCsvFile(name, lines) @@ -356,65 +365,18 @@ const csv = { countMode = 'count', production ) { - let entries = [] - const taskTypeIds = getStatsTaskTypeIds(mainStats, taskTypeMap, production) - const entryIds = getStatsEntryIds(mainStats, entryMap) - - entryIds.forEach(entryId => { - const taskStatusIds = getStatsTaskStatusIdsForEntry( - mainStats, - entryId - ).filter(s => !['max_retake_count', 'evolution'].includes(s)) - const total = getStatsTotalCount( - mainStats, - taskStatusIds, - countMode, - entryId - ) - const lineMap = buildTotalLines( - entryMap, - taskStatusMap, - countMode, - mainStats, - taskStatusIds, - entryId, - total - ) - - taskTypeIds.forEach(taskTypeId => { - if (taskTypeId !== 'all') { - const taskTypeStats = mainStats[entryId][taskTypeId] - if (taskTypeStats) { - const total = getStatsTotalEntryCount( - mainStats, - taskTypeStats, - countMode, - entryId, - taskTypeId - ) - addEntryStatusStats( - mainStats, - countMode, - entryId, - taskTypeId, - taskStatusIds, - total, - lineMap - ) - } else { - Object.keys(mainStats[entryId].all).forEach(taskStatusId => { - if (!['max_retake_count', 'evolution'].includes(taskStatusId)) { - lineMap[taskStatusId] = lineMap[taskStatusId].concat(['', '']) - } - }) - } - } - }) - - entries = entries.concat(Object.values(lineMap)) - entries.push(['']) - }) - return entries + return csv.getStatReportsEntries( + mainStats, + taskTypeMap, + taskStatusMap, + entryMap, + countMode, + production, + { + excludeStatuses: ['max_retake_count', 'evolution'], + sortEntries: false + } + ) }, generateQuotas( diff --git a/src/lib/indexing.js b/src/lib/indexing.js index 8caac1515f..705b1b9d4b 100644 --- a/src/lib/indexing.js +++ b/src/lib/indexing.js @@ -1,9 +1,13 @@ +const createIndexPair = () => ({ + index: Object.create(null), + entryIndex: Object.create(null) +}) + /* * Build a simple index based on entry names. */ export const buildNameIndex = (entries, split = true, withEmail = false) => { - const index = Object.create(null) - const entryIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() entries.forEach(entry => { if (entry) { let words @@ -64,8 +68,7 @@ export const buildTaskStatusIndex = taskStatuses => { * The result is an array of tasks. */ export const buildTaskIndex = tasks => { - const index = Object.create(null) - const taskIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() tasks.forEach(task => { const stringToIndex = task.full_entity_name .replace(/_/g, ' ') @@ -78,7 +81,7 @@ export const buildTaskIndex = tasks => { task.task_status_short_name, task.project_name ]) - indexWords(index, taskIndex, task, words) + indexWords(index, entryIndex, task, words) }) return index } @@ -89,8 +92,7 @@ export const buildTaskIndex = tasks => { * The result is an array of tasks. */ export const buildSupervisorTaskIndex = (tasks, personMap, taskStatusMap) => { - const index = Object.create(null) - const taskIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() tasks.forEach(task => { const stringToIndex = task.entity_name.replace(/_/g, ' ').replace(/-/g, ' ') const taskStatus = taskStatusMap.get(task.task_status_id) @@ -102,7 +104,7 @@ export const buildSupervisorTaskIndex = (tasks, personMap, taskStatusMap) => { const person = personMap.get(personId) if (person) words.push(person.first_name, person.last_name) }) - indexWords(index, taskIndex, task, words) + indexWords(index, entryIndex, task, words) }) return index } @@ -113,8 +115,7 @@ export const buildSupervisorTaskIndex = (tasks, personMap, taskStatusMap) => { * Results are arrays of assets. */ export const buildAssetIndex = entries => { - const index = Object.create(null) - const assetIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() entries.forEach(asset => { const stringToIndex = asset.name.replace(/_/g, ' ').replace(/-/g, ' ') let words = [] @@ -124,7 +125,7 @@ export const buildAssetIndex = entries => { const camelWords = stringToIndex.match(/[A-Z]+[a-z0-9]*/g) if (camelWords) words = words.concat(camelWords) words = [...new Set(words.map(word => word.toLowerCase()))] - indexWords(index, assetIndex, asset, words) + indexWords(index, entryIndex, asset, words) }) return index } @@ -135,11 +136,10 @@ export const buildAssetIndex = entries => { * Results are arrays of shots. */ export const buildShotIndex = shots => { - const index = Object.create(null) - const shotIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() shots.forEach(shot => { const words = [shot.name, shot.sequence_name, shot.episode_name] - indexWords(index, shotIndex, shot, words) + indexWords(index, entryIndex, shot, words) }) return index } @@ -150,11 +150,10 @@ export const buildShotIndex = shots => { * Results are arrays of edits. */ export const buildEditIndex = edits => { - const index = Object.create(null) - const editIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() edits.forEach(edit => { const words = [edit.name, edit.episode_name] - indexWords(index, editIndex, edit, words) + indexWords(index, entryIndex, edit, words) }) return index } @@ -165,11 +164,10 @@ export const buildEditIndex = edits => { * Results are arrays of sequences. */ export const buildSequenceIndex = sequences => { - const index = Object.create(null) - const sequenceIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() sequences.forEach(sequence => { const words = [sequence.name, sequence.episode_name] - indexWords(index, sequenceIndex, sequence, words) + indexWords(index, entryIndex, sequence, words) }) return index } @@ -180,11 +178,10 @@ export const buildSequenceIndex = sequences => { * Results are arrays of episodes. */ export const buildEpisodeIndex = episodes => { - const index = Object.create(null) - const episodeIndex = Object.create(null) + const { index, entryIndex } = createIndexPair() episodes.forEach(episode => { const words = [episode.name] - indexWords(index, episodeIndex, episode, words) + indexWords(index, entryIndex, episode, words) }) return index } diff --git a/src/lib/sorting.js b/src/lib/sorting.js index 2dc3821046..8cd9586d07 100644 --- a/src/lib/sorting.js +++ b/src/lib/sorting.js @@ -261,109 +261,70 @@ export const sortValidationColumns = ( }) } -export const sortAssetResult = (result, sorting, taskTypeMap, taskMap) => { +const compareByName = (a, b) => + a.name.localeCompare(b.name, undefined, { numeric: true }) + +const sortEntityResult = (result, sorting, taskMap, thenBySteps, defaultSort) => { if (sorting && sorting.length > 0) { const sortInfo = sorting[0] const sortEntities = sortInfo.type === 'metadata' ? sortByMetadata(sortInfo) : sortByTaskType(taskMap, sortInfo) - result = result.sort( - firstBy('canceled') - .thenBy(sortEntities) - .thenBy((a, b) => - a.asset_type_name.localeCompare(b.asset_type_name, undefined, { - numeric: true - }) - ) - .thenBy((a, b) => - a.name.localeCompare(b.name, undefined, { numeric: true }) - ) - ) + let sorter = firstBy('canceled').thenBy(sortEntities) + for (const step of thenBySteps) { + sorter = sorter.thenBy(step) + } + result = result.sort(sorter) } else { - result = sortAssets(result) + result = defaultSort(result) } return result } +export const sortAssetResult = (result, sorting, taskTypeMap, taskMap) => { + return sortEntityResult(result, sorting, taskMap, [ + (a, b) => + a.asset_type_name.localeCompare(b.asset_type_name, undefined, { + numeric: true + }), + compareByName + ], sortAssets) +} + export const sortShotResult = (result, sorting, taskTypeMap, taskMap) => { - if (sorting && sorting.length > 0) { - const sortInfo = sorting[0] - let sortEntities = sortByTaskType(taskMap, sortInfo) - if (sortInfo.type === 'metadata') sortEntities = sortByMetadata(sortInfo) - result = result.sort( - firstBy('canceled') - .thenBy(sortEntities) - .thenBy(sortByEpisode) - .thenBy((a, b) => - a.sequence_name.localeCompare(b.sequence_name, undefined, { - numeric: true - }) - ) - .thenBy((a, b) => - a.name.localeCompare(b.name, undefined, { numeric: true }) - ) - ) - } else { - result = sortShots(result) - } - return result + return sortEntityResult(result, sorting, taskMap, [ + sortByEpisode, + (a, b) => + a.sequence_name.localeCompare(b.sequence_name, undefined, { + numeric: true + }), + compareByName + ], sortShots) } export const sortSequenceResult = (result, sorting, taskTypeMap, taskMap) => { - if (sorting && sorting.length > 0) { - const sortInfo = sorting[0] - let sortEntities = sortByTaskType(taskMap, sortInfo) - if (sortInfo.type === 'metadata') sortEntities = sortByMetadata(sortInfo) - result = result.sort( - firstBy('canceled') - .thenBy(sortEntities) - .thenBy(sortByEpisode) - .thenBy((a, b) => - a.name.localeCompare(b.name, undefined, { numeric: true }) - ) - ) - } else { - result = sortByName(result) - } - return result + return sortEntityResult( + result, sorting, taskMap, + [sortByEpisode, compareByName], + sortByName + ) } export const sortEpisodeResult = (result, sorting, taskTypeMap, taskMap) => { - if (sorting && sorting.length > 0) { - const sortInfo = sorting[0] - let sortEntities = sortByTaskType(taskMap, sortInfo) - if (sortInfo.type === 'metadata') sortEntities = sortByMetadata(sortInfo) - result = result.sort( - firstBy('canceled') - .thenBy(sortEntities) - .thenBy((a, b) => - a.name.localeCompare(b.name, undefined, { numeric: true }) - ) - ) - } else { - result = sortByName(result) - } - return result + return sortEntityResult( + result, sorting, taskMap, + [compareByName], + sortByName + ) } export const sortEditResult = (result, sorting, taskTypeMap, taskMap) => { - if (sorting && sorting.length > 0) { - const sortInfo = sorting[0] - let sortEntities = sortByTaskType(taskMap, sortInfo) - if (sortInfo.type === 'metadata') sortEntities = sortByMetadata(sortInfo) - result = result.sort( - firstBy('canceled') - .thenBy(sortEntities) - .thenBy(sortByEpisode) - .thenBy((a, b) => - a.name.localeCompare(b.name, undefined, { numeric: true }) - ) - ) - } else { - result = sortEdits(result) - } - return result + return sortEntityResult( + result, sorting, taskMap, + [sortByEpisode, compareByName], + sortEdits + ) } const getMetadataValues = (sortInfo, a, b, defaultValue = '') => { diff --git a/src/lib/time.js b/src/lib/time.js index 1eac4d9998..24eaab8398 100644 --- a/src/lib/time.js +++ b/src/lib/time.js @@ -223,17 +223,15 @@ export const getBusinessDays = (startDate, endDate, daysOff = []) => { return nbDays } -export const addBusinessDays = (originalDate, numDaysToAdd, daysOff = []) => { - if (!originalDate) { - return - } +const adjustBusinessDays = (originalDate, numDays, daysOff, method) => { + if (!originalDate) return const Sunday = 0 const Saturday = 6 const datesOff = daysOff ? getDayOffRange(daysOff).map(dayOff => dayOff.date) : [] const newDate = originalDate.clone() - let daysRemaining = numDaysToAdd + let daysRemaining = numDays while (daysRemaining >= 0) { if ( newDate.day() !== Sunday && @@ -243,37 +241,22 @@ export const addBusinessDays = (originalDate, numDaysToAdd, daysOff = []) => { daysRemaining-- } if (daysRemaining >= 0) { - newDate.add(1, 'days') + newDate[method](1, 'days') } } return newDate } +export const addBusinessDays = (originalDate, numDaysToAdd, daysOff = []) => { + return adjustBusinessDays(originalDate, numDaysToAdd, daysOff, 'add') +} + export const removeBusinessDays = ( originalDate, numDaysToRemove, daysOff = [] ) => { - const Sunday = 0 - const Saturday = 6 - const datesOff = daysOff - ? getDayOffRange(daysOff).map(dayOff => dayOff.date) - : [] - const newDate = originalDate.clone() - let daysRemaining = numDaysToRemove - while (daysRemaining >= 0) { - if ( - newDate.day() !== Sunday && - newDate.day() !== Saturday && - !datesOff.includes(newDate.format('YYYY-MM-DD')) - ) { - daysRemaining-- - } - if (daysRemaining >= 0) { - newDate.subtract(1, 'days') - } - } - return newDate + return adjustBusinessDays(originalDate, numDaysToRemove, daysOff, 'subtract') } export const getDayOffRange = (daysOff = []) => { From 3a7244ec80c7532ccebd36135a82a8b4c72b9da7 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 03:02:17 +0100 Subject: [PATCH 04/92] [lib] improve readability in query, models, stats, and colors - query.js: rename 'couples' to 'pairs' for standard naming - models.js: simplify populateTask with array-based name building - stats.js: extract createStatusEntry factory for repeated init blocks - colors.js: simplify fromString with single conditional ColorHash instance Co-Authored-By: Claude Opus 4.6 --- src/lib/colors.js | 26 ++++++------------ src/lib/models.js | 34 +++++++++++------------ src/lib/query.js | 8 +++--- src/lib/stats.js | 69 +++++++++++++++++------------------------------ 4 files changed, 54 insertions(+), 83 deletions(-) diff --git a/src/lib/colors.js b/src/lib/colors.js index c09f249564..187c2c496a 100644 --- a/src/lib/colors.js +++ b/src/lib/colors.js @@ -9,6 +9,8 @@ const fadeColorIndex = {} let colorHashConstructor = ColorHash if (ColorHash.default) colorHashConstructor = ColorHash.default +const DARK_STATUS_NAMES = ['todo', 'wtg'] + export default { /* * Turn hexadecimal color (#FFFFFF) to a darker and more saturated version. @@ -26,19 +28,12 @@ export default { * Convert a string (it can be anything) into a HTML color hash. */ fromString(str, darken = false) { - let colorHash = new colorHashConstructor({ - lightness: 0.7, + const isDark = + darken || (localStorage && localStorage.getItem('dark-theme') === 'true') + const colorHash = new colorHashConstructor({ + lightness: isDark ? 0.6 : 0.7, saturation: 0.8 }) - if ( - darken || - (localStorage && localStorage.getItem('dark-theme') === 'true') - ) { - colorHash = new colorHashConstructor({ - lightness: 0.6, - saturation: 0.8 - }) - } return colorHash.hex(str) }, @@ -86,14 +81,9 @@ export default { * is too dark. */ validationTextColor(task) { - if ( - task && - task.task_status_short_name !== 'todo' && - task.task_status_short_name !== 'wtg' - ) { + if (task && !DARK_STATUS_NAMES.includes(task.task_status_short_name)) { return 'white' - } else { - return '#333' } + return '#333' } } diff --git a/src/lib/models.js b/src/lib/models.js index d62ac4ec2a..3c095ab371 100644 --- a/src/lib/models.js +++ b/src/lib/models.js @@ -1,29 +1,27 @@ export const populateTask = task => { - if (task.entity_type_name === 'Shot') { - if (task.episode_name) { - task.full_entity_name = `${task.episode_name} / ${task.sequence_name} / ${task.entity_name}` - } else { - task.full_entity_name = `${task.sequence_name} / ${task.entity_name}` - } - } else if (task.entity_type_name === 'Episode') { - task.full_entity_name = `${task.entity_name}` - } else if (['Sequence', 'Edit'].includes(task.entity_type_name)) { - if (task.episode_name) { - task.full_entity_name = `${task.episode_name} / ${task.entity_name}` - } else { - task.full_entity_name = `${task.entity_name}` - } + const type = task.entity_type_name + const parts = [] + + if (type === 'Shot') { + if (task.episode_name) parts.push(task.episode_name) + parts.push(task.sequence_name, task.entity_name) + } else if (['Sequence', 'Edit'].includes(type)) { + if (task.episode_name) parts.push(task.episode_name) + parts.push(task.entity_name) + } else if (type === 'Episode') { + parts.push(task.entity_name) } else { - task.full_entity_name = `${task.entity_type_name} / ${task.entity_name}` + parts.push(task.entity_type_name, task.entity_name) } - const type = task.entity_type_name.toLowerCase() + + task.full_entity_name = parts.join(' / ') task.entity_path = { - name: type, + name: type.toLowerCase(), params: { production_id: task.project_id } } - task.entity_path.params[`${type}_id`] = task.entity_id + task.entity_path.params[`${type.toLowerCase()}_id`] = task.entity_id return task } diff --git a/src/lib/query.js b/src/lib/query.js index 8b8ef1a5a6..cad7e00be8 100644 --- a/src/lib/query.js +++ b/src/lib/query.js @@ -9,13 +9,15 @@ */ export const buildQueryString = (path, params) => { const result = `${path}?` - const couples = [] + const pairs = [] Object.keys(params).forEach(key => { if ( params[key] || (typeof params[key] === 'boolean' && params[key] === false) ) - couples.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + pairs.push( + `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` + ) }) - return result + couples.join('&') + return result + pairs.join('&') } diff --git a/src/lib/stats.js b/src/lib/stats.js index b356db35ae..0f71cd2da4 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -1,3 +1,19 @@ +const RETAKE_CHART_COLORS = { + done: '#22d160', + retake: '#ff3860', + other: '#6f727a' +} + +const DEFAULT_STATUS_COLOR = '#6F727A' + +const createStatusEntry = taskStatus => ({ + name: taskStatus.short_name, + color: taskStatus.color, + count: 0, + frames: 0, + drawings: 0 +}) + // Get all data displayed in statistics (needed by the stat cell widget). // Data follow this format: [[task-status-1-name, value], ...] // Set count data or frames data depending on data type. @@ -13,7 +29,7 @@ export const getChartData = ( return Object.keys(statusData) .map(taskStatusId => { const data = statusData[taskStatusId] - const color = data.is_default ? '#6F727A' : data.color + const color = data.is_default ? DEFAULT_STATUS_COLOR : data.color return [data.name, data[valueField], color] }) .sort(_sortData) @@ -37,20 +53,15 @@ export const getRetakeChartData = ( columnId, dataType = 'count' ) => { - const colorMap = { - done: '#22d160', - retake: '#ff3860', - other: '#6f727a' - } if (!mainStats[entryId] || !mainStats[entryId][columnId]) return [] const statusData = { ...mainStats[entryId][columnId] } delete statusData.evolution delete statusData.max_retake_count const valueField = dataType return [ - ['retake', statusData.retake[valueField] || 0, colorMap.retake], - ['other', statusData.other[valueField] || 0, colorMap.other], - ['done', statusData.done[valueField] || 0, colorMap.done] + ['retake', statusData.retake[valueField] || 0, RETAKE_CHART_COLORS.retake], + ['other', statusData.other[valueField] || 0, RETAKE_CHART_COLORS.other], + ['done', statusData.done[valueField] || 0, RETAKE_CHART_COLORS.done] ] } @@ -133,51 +144,21 @@ const computeTaskResult = ( results[sequenceId][taskTypeId] = {} } - // All / all intersections if (!results.all.all[taskStatusId]) { - results.all.all[taskStatusId] = { - name: taskStatus.short_name, - color: taskStatus.color, - count: 0, - frames: 0, - drawings: 0 - } + results.all.all[taskStatusId] = createStatusEntry(taskStatus) } - - // All line if (!results.all[taskTypeId]) { results.all[taskTypeId] = {} } if (!results.all[taskTypeId][taskStatusId]) { - results.all[taskTypeId][taskStatusId] = { - name: taskStatus.short_name, - color: taskStatus.color, - count: 0, - frames: 0, - drawings: 0 - } + results.all[taskTypeId][taskStatusId] = createStatusEntry(taskStatus) } - - // All column if (!results[sequenceId].all[taskStatusId]) { - results[sequenceId].all[taskStatusId] = { - name: taskStatus.short_name, - color: taskStatus.color, - count: 0, - frames: 0, - drawings: 0 - } + results[sequenceId].all[taskStatusId] = createStatusEntry(taskStatus) } - - // Columns if (!results[sequenceId][taskTypeId][taskStatusId]) { - results[sequenceId][taskTypeId][taskStatusId] = { - name: taskStatus.short_name, - color: taskStatus.color, - count: 0, - frames: 0, - drawings: 0 - } + results[sequenceId][taskTypeId][taskStatusId] = + createStatusEntry(taskStatus) } // Slice count From 04f93bc2fb80c7584783ccd465bbc02661e2bab0 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 03:02:31 +0100 Subject: [PATCH 05/92] [lib] improve maintainability in drafts, preferences, and colors - drafts.js: add try-catch for localStorage operations (quota/security) - preferences.js: add radix parameter to parseInt - videoCache.js: remove console.log left in production code - colors.js: extract DARK_STATUS_NAMES constant for validationTextColor Co-Authored-By: Claude Opus 4.6 --- src/lib/drafts.js | 21 ++++++++++++++++++--- src/lib/preferences.js | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/lib/drafts.js b/src/lib/drafts.js index 81655a9c37..3d5e5d2f46 100644 --- a/src/lib/drafts.js +++ b/src/lib/drafts.js @@ -1,14 +1,29 @@ +const DRAFT_PREFIX = 'draft-' + const drafts = { setTaskDraft(taskId, text) { - return localStorage.setItem('draft-' + taskId, text) + try { + return localStorage.setItem(DRAFT_PREFIX + taskId, text) + } catch (e) { + console.warn('Failed to save draft:', e) + } }, getTaskDraft(taskId) { - return localStorage.getItem('draft-' + taskId) + try { + return localStorage.getItem(DRAFT_PREFIX + taskId) + } catch (e) { + console.warn('Failed to read draft:', e) + return null + } }, clearTaskDraft(taskId) { - return localStorage.removeItem('draft-' + taskId) + try { + return localStorage.removeItem(DRAFT_PREFIX + taskId) + } catch (e) { + console.warn('Failed to clear draft:', e) + } } } diff --git a/src/lib/preferences.js b/src/lib/preferences.js index 48ef4bb321..1fb32a62d4 100644 --- a/src/lib/preferences.js +++ b/src/lib/preferences.js @@ -14,7 +14,7 @@ export default { getIntPreference(key, defaultValue = 0) { const item = this.getPreference(key) - const value = parseInt(item) + const value = parseInt(item, 10) return isNaN(value) ? defaultValue : value }, From 43464021a2f9b6b7a774f6dc5ecd4feb328c7e21 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 03:02:38 +0100 Subject: [PATCH 06/92] [lib] extract magic numbers into named constants - video.js: extract DEFAULT_FPS constant (25) - stats.js: extract RETAKE_CHART_COLORS and DEFAULT_STATUS_COLOR constants - time.js: extract SUNDAY/SATURDAY as module-level constants, use isoWeeksInYear() instead of hardcoded 52 Co-Authored-By: Claude Opus 4.6 --- src/lib/time.js | 17 ++++++++--------- src/lib/video.js | 5 +++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/time.js b/src/lib/time.js index 24eaab8398..f9a37066fd 100644 --- a/src/lib/time.js +++ b/src/lib/time.js @@ -1,5 +1,8 @@ import moment from 'moment-timezone' +const SUNDAY = 0 +const SATURDAY = 6 + export const range = (start, end) => { let length = end - start + 1 if (length < 0) length = 0 @@ -82,7 +85,7 @@ export const getWeekRange = (year, currentYear) => { if (currentYear === year) { return range(1, moment().week()) } else { - return range(1, 52) + return range(1, moment(String(year), 'YYYY').isoWeeksInYear()) } } @@ -203,8 +206,6 @@ export const getDatesFromEndDate = ( } export const getBusinessDays = (startDate, endDate, daysOff = []) => { - const Sunday = 0 - const Saturday = 6 const datesOff = daysOff ? getDayOffRange(daysOff).map(dayOff => dayOff.date) : [] @@ -212,8 +213,8 @@ export const getBusinessDays = (startDate, endDate, daysOff = []) => { let nbDays = 0 while (newDate.isSameOrBefore(endDate)) { if ( - newDate.day() !== Sunday && - newDate.day() !== Saturday && + newDate.day() !== SUNDAY && + newDate.day() !== SATURDAY && !datesOff.includes(newDate.format('YYYY-MM-DD')) ) { nbDays++ @@ -225,8 +226,6 @@ export const getBusinessDays = (startDate, endDate, daysOff = []) => { const adjustBusinessDays = (originalDate, numDays, daysOff, method) => { if (!originalDate) return - const Sunday = 0 - const Saturday = 6 const datesOff = daysOff ? getDayOffRange(daysOff).map(dayOff => dayOff.date) : [] @@ -234,8 +233,8 @@ const adjustBusinessDays = (originalDate, numDays, daysOff, method) => { let daysRemaining = numDays while (daysRemaining >= 0) { if ( - newDate.day() !== Sunday && - newDate.day() !== Saturday && + newDate.day() !== SUNDAY && + newDate.day() !== SATURDAY && !datesOff.includes(newDate.format('YYYY-MM-DD')) ) { daysRemaining-- diff --git a/src/lib/video.js b/src/lib/video.js index ecdd4ecdbb..2e89a2bf62 100644 --- a/src/lib/video.js +++ b/src/lib/video.js @@ -1,4 +1,5 @@ const PRECISION_FACTOR = 10000 +const DEFAULT_FPS = 25 const roundPrecision = value => Math.round(value * PRECISION_FACTOR) / PRECISION_FACTOR @@ -28,7 +29,7 @@ export const floorToFrame = (time, fps) => { * Turn a frame number into seconds depending on context. */ export const frameToSeconds = (nbFrames, production, shot) => { - let fps = 25 + let fps = DEFAULT_FPS if (shot && shot.fps) fps = shot.fps if (production && production.fps) fps = production.fps return Math.round((nbFrames / fps) * 1000) / 1000 @@ -58,7 +59,7 @@ export const formatTime = (rawTime, fps) => { /** * Get timecode from frame number. */ -export const formatToTimecode = (frame, fps = 25) => { +export const formatToTimecode = (frame, fps = DEFAULT_FPS) => { if (!frame || frame < 0) frame = 0 const hours = Math.floor(frame / fps / 3600) .toString() From c5b3c800562487da54d3f3486e0344e787e120c9 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 03:04:22 +0100 Subject: [PATCH 07/92] [lib] minor cleanups in func, descriptors, and crisp - func.js: use Date.now() instead of new Date().getTime() - descriptors.js: add guard for missing descriptor.choices - crisp.js: add onerror handler for script load failure, remove unnecessary IIFE Co-Authored-By: Claude Opus 4.6 --- src/lib/crisp.js | 12 +++++------- src/lib/descriptors.js | 1 + src/lib/func.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/crisp.js b/src/lib/crisp.js index 4d9be1a9bc..301ccb6743 100644 --- a/src/lib/crisp.js +++ b/src/lib/crisp.js @@ -2,13 +2,11 @@ export default { init(token) { window.$crisp = [] window.CRISP_WEBSITE_ID = token - ;(function () { - const d = document - const s = d.createElement('script') - s.src = 'https://client.crisp.chat/l.js' - s.async = true - d.getElementsByTagName('head')[0].appendChild(s) - })() + const s = document.createElement('script') + s.src = 'https://client.crisp.chat/l.js' + s.async = true + s.onerror = () => console.warn('Failed to load Crisp chat widget') + document.getElementsByTagName('head')[0].appendChild(s) }, setChatVisibility(isVisible) { diff --git a/src/lib/descriptors.js b/src/lib/descriptors.js index 08f3dbe223..281b3a4256 100644 --- a/src/lib/descriptors.js +++ b/src/lib/descriptors.js @@ -1,5 +1,6 @@ export default { getChoicesOptions(descriptor) { + if (!descriptor?.choices) return [{ label: '', value: '' }] const values = descriptor.choices.map(c => ({ label: c, value: c })) return [{ label: '', value: '' }, ...values] } diff --git a/src/lib/func.js b/src/lib/func.js index ecaf24d577..2afca68fb4 100644 --- a/src/lib/func.js +++ b/src/lib/func.js @@ -14,7 +14,7 @@ export default { throttle(fn, delay) { let lastCall = 0 return function (...args) { - const now = new Date().getTime() + const now = Date.now() if (now - lastCall < delay) return lastCall = now return fn(...args) From e3688858ba2c3f1d05869ab9621f2199fa103085 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 09:41:01 +0100 Subject: [PATCH 08/92] [tests] improve lib test coverage with new and expanded tests Add 4 new test files (clipboard, drafts, productions, errors) and expand 7 existing test files with additional edge case coverage. Co-Authored-By: Claude Opus 4.6 --- tests/unit/lib/clipboard.spec.js | 29 ++++++++++++++++ tests/unit/lib/colors.spec.js | 9 +++++ tests/unit/lib/descriptor.spec.js | 9 +++++ tests/unit/lib/drafts.spec.js | 50 ++++++++++++++++++++++++++ tests/unit/lib/errors.spec.js | 25 +++++++++++++ tests/unit/lib/func.spec.js | 22 ++++++++++++ tests/unit/lib/models.spec.js | 50 ++++++++++++++++++++++++++ tests/unit/lib/productions.spec.js | 56 ++++++++++++++++++++++++++++++ tests/unit/lib/query.spec.js | 15 ++++++++ tests/unit/lib/stats.spec.js | 10 +++++- tests/unit/lib/video.spec.js | 27 ++++++++++++++ 11 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 tests/unit/lib/clipboard.spec.js create mode 100644 tests/unit/lib/drafts.spec.js create mode 100644 tests/unit/lib/errors.spec.js create mode 100644 tests/unit/lib/productions.spec.js diff --git a/tests/unit/lib/clipboard.spec.js b/tests/unit/lib/clipboard.spec.js new file mode 100644 index 0000000000..0c2024812b --- /dev/null +++ b/tests/unit/lib/clipboard.spec.js @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest' + +import clipboard from '@/lib/clipboard' + +describe('clipboard', () => { + describe('annotations', () => { + it('returns an empty array initially', () => { + expect(clipboard.pasteAnnotations()).toEqual([]) + }) + + it('returns the same reference after copy', () => { + const annotations = [{ id: 1, text: 'note' }] + clipboard.copyAnnotations(annotations) + expect(clipboard.pasteAnnotations()).toBe(annotations) + }) + }) + + describe('casting', () => { + it('returns an empty array initially', () => { + expect(clipboard.pasteCasting()).toEqual([]) + }) + + it('returns the same reference after copy', () => { + const casting = [{ asset_id: 'a1', nb_occurences: 2 }] + clipboard.copyCasting(casting) + expect(clipboard.pasteCasting()).toBe(casting) + }) + }) +}) diff --git a/tests/unit/lib/colors.spec.js b/tests/unit/lib/colors.spec.js index fd95c993e5..a2f0c8d36e 100644 --- a/tests/unit/lib/colors.spec.js +++ b/tests/unit/lib/colors.spec.js @@ -47,4 +47,13 @@ describe('colors', () => { expect(colors.fromString('123456')).toEqual('#f0ee75') expect(colors.fromString('Jhon Doe')).toEqual('#9e75f0') }) + + test('validationTextColor with wtg status', () => { + const task = { task_status_short_name: 'wtg' } + expect(colors.validationTextColor(task)).toEqual('#333') + }) + + test('validationTextColor with null task', () => { + expect(colors.validationTextColor(null)).toEqual('#333') + }) }) diff --git a/tests/unit/lib/descriptor.spec.js b/tests/unit/lib/descriptor.spec.js index bed6f9dfd3..7bd4f17b57 100644 --- a/tests/unit/lib/descriptor.spec.js +++ b/tests/unit/lib/descriptor.spec.js @@ -13,4 +13,13 @@ describe('descriptors', () => { { label: 'difficult', value: 'difficult' } ]) }) + + test('getChoiceOptions with missing choices', () => { + expect(descriptors.getChoicesOptions({})).toEqual([ + { label: '', value: '' } + ]) + expect(descriptors.getChoicesOptions(null)).toEqual([ + { label: '', value: '' } + ]) + }) }) diff --git a/tests/unit/lib/drafts.spec.js b/tests/unit/lib/drafts.spec.js new file mode 100644 index 0000000000..9659a0d91b --- /dev/null +++ b/tests/unit/lib/drafts.spec.js @@ -0,0 +1,50 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +import drafts from '@/lib/drafts' + +describe('drafts', () => { + beforeEach(() => { + localStorage.clear() + }) + + it('sets and gets a task draft', () => { + drafts.setTaskDraft('task-1', 'my draft text') + expect(drafts.getTaskDraft('task-1')).toBe('my draft text') + }) + + it('returns null for a non-existent draft', () => { + expect(drafts.getTaskDraft('unknown-id')).toBeNull() + }) + + it('clears a task draft', () => { + drafts.setTaskDraft('task-2', 'some text') + drafts.clearTaskDraft('task-2') + expect(drafts.getTaskDraft('task-2')).toBeNull() + }) + + it('uses the draft- prefix in localStorage', () => { + drafts.setTaskDraft('task-3', 'hello') + expect(localStorage.getItem('draft-task-3')).toBe('hello') + }) + + it('handles localStorage errors gracefully on set', () => { + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new Error('QuotaExceeded') + }) + expect(() => drafts.setTaskDraft('task-4', 'text')).not.toThrow() + }) + + it('handles localStorage errors gracefully on get', () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('SecurityError') + }) + expect(drafts.getTaskDraft('task-5')).toBeNull() + }) + + it('handles localStorage errors gracefully on clear', () => { + vi.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { + throw new Error('SecurityError') + }) + expect(() => drafts.clearTaskDraft('task-6')).not.toThrow() + }) +}) diff --git a/tests/unit/lib/errors.spec.js b/tests/unit/lib/errors.spec.js new file mode 100644 index 0000000000..b490207be6 --- /dev/null +++ b/tests/unit/lib/errors.spec.js @@ -0,0 +1,25 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +import errors from '@/lib/errors' + +describe('errors', () => { + beforeEach(() => { + delete window.location + window.location = { + pathname: '/', + replace: vi.fn() + } + }) + + it('calls replace with /login when not on the login page', () => { + window.location.pathname = '/productions' + errors.backToLogin() + expect(window.location.replace).toHaveBeenCalledWith('/login') + }) + + it('does not call replace when already on /login', () => { + window.location.pathname = '/login' + errors.backToLogin() + expect(window.location.replace).not.toHaveBeenCalled() + }) +}) diff --git a/tests/unit/lib/func.spec.js b/tests/unit/lib/func.spec.js index 2743c29a09..d8b17cf3f5 100644 --- a/tests/unit/lib/func.spec.js +++ b/tests/unit/lib/func.spec.js @@ -13,4 +13,26 @@ describe('func', () => { done() }) })) + + test('throttle', () => { + let count = 0 + const fn = func.throttle(() => { count++ }, 100) + fn() + expect(count).toBe(1) + fn() + expect(count).toBe(1) // throttled + }) + + test('runPromiseAsSeries', () => new Promise((done) => { + let counter = 0 + const promises = [ + Promise.resolve().then(() => { counter += 1 }), + Promise.resolve().then(() => { counter += 5 }) + ] + func.runPromiseAsSeries(promises) + .then(() => { + expect(counter).toEqual(6) + done() + }) + })) }) diff --git a/tests/unit/lib/models.spec.js b/tests/unit/lib/models.spec.js index ca2080572e..de5a3a3afa 100644 --- a/tests/unit/lib/models.spec.js +++ b/tests/unit/lib/models.spec.js @@ -1,5 +1,6 @@ import { addToIdList, + arrayMove, removeFromIdList, getFilledColumns, groupEntitiesByParents, @@ -153,4 +154,53 @@ describe('lib/helpers', () => { removeFromIdList(production, 'asset_types', 'type-2') expect(production.asset_types).toEqual(['type-1', 'type-3']) }) + + it('populateTask - Episode', () => { + const task = { + entity_name: 'E01', + entity_type_name: 'Episode', + project_id: 'prod-1', + entity_id: 'ep-1' + } + populateTask(task) + expect(task.full_entity_name).toEqual('E01') + }) + + it('populateTask - Sequence with episode', () => { + const task = { + entity_name: 'SQ01', + entity_type_name: 'Sequence', + episode_name: 'E01', + project_id: 'prod-1', + entity_id: 'seq-1' + } + populateTask(task) + expect(task.full_entity_name).toEqual('E01 / SQ01') + }) + + it('populateTask - Edit without episode', () => { + const task = { + entity_name: 'ED01', + entity_type_name: 'Edit', + project_id: 'prod-1', + entity_id: 'edit-1' + } + populateTask(task) + expect(task.full_entity_name).toEqual('ED01') + }) + + it('removeFromIdList - id not found', () => { + const production = { + id: '1', + asset_types: ['type-1', 'type-2'] + } + removeFromIdList(production, 'asset_types', 'type-999') + expect(production.asset_types).toEqual(['type-1', 'type-2']) + }) + + it('arrayMove', () => { + const arr = ['a', 'b', 'c', 'd'] + expect(arrayMove(arr, 0, 2)).toEqual(['b', 'c', 'a', 'd']) + expect(arr).toEqual(['a', 'b', 'c', 'd']) // original unchanged + }) }) diff --git a/tests/unit/lib/productions.spec.js b/tests/unit/lib/productions.spec.js new file mode 100644 index 0000000000..0816fadb8c --- /dev/null +++ b/tests/unit/lib/productions.spec.js @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest' + +import { + getTaskTypePriorityOfProd, + getTaskStatusPriorityOfProd +} from '@/lib/productions' + +describe('productions', () => { + describe('getTaskTypePriorityOfProd', () => { + it('returns 1 when taskType is null', () => { + expect(getTaskTypePriorityOfProd(null, {})).toBe(1) + }) + + it('returns production-level priority when set', () => { + const taskType = { id: 'tt-1', priority: 5 } + const production = { task_types_priority: { 'tt-1': 3 } } + expect(getTaskTypePriorityOfProd(taskType, production)).toBe(3) + }) + + it('falls back to taskType priority when production has no override', () => { + const taskType = { id: 'tt-2', priority: 7 } + const production = { task_types_priority: {} } + expect(getTaskTypePriorityOfProd(taskType, production)).toBe(7) + }) + + it('falls back to taskType priority when production is null', () => { + const taskType = { id: 'tt-3', priority: 4 } + expect(getTaskTypePriorityOfProd(taskType, null)).toBe(4) + }) + }) + + describe('getTaskStatusPriorityOfProd', () => { + it('returns 1 when taskStatus is null', () => { + expect(getTaskStatusPriorityOfProd(null, {})).toBe(1) + }) + + it('returns production-level priority when set', () => { + const taskStatus = { id: 'ts-1', priority: 5 } + const production = { + task_statuses_link: { 'ts-1': { priority: 2 } } + } + expect(getTaskStatusPriorityOfProd(taskStatus, production)).toBe(2) + }) + + it('falls back to taskStatus priority when production has no override', () => { + const taskStatus = { id: 'ts-2', priority: 8 } + const production = { task_statuses_link: {} } + expect(getTaskStatusPriorityOfProd(taskStatus, production)).toBe(8) + }) + + it('falls back to taskStatus priority when production is null', () => { + const taskStatus = { id: 'ts-3', priority: 6 } + expect(getTaskStatusPriorityOfProd(taskStatus, null)).toBe(6) + }) + }) +}) diff --git a/tests/unit/lib/query.spec.js b/tests/unit/lib/query.spec.js index 7a2568e2c7..3298666884 100644 --- a/tests/unit/lib/query.spec.js +++ b/tests/unit/lib/query.spec.js @@ -9,5 +9,20 @@ describe('query', () => { }) expect(path).toEqual('/data?page=2&id=entity-id') }) + + test('buildQueryString with special characters', () => { + const path = buildQueryString('/data', { name: 'hello world', tag: 'a&b' }) + expect(path).toEqual('/data?name=hello%20world&tag=a%26b') + }) + + test('buildQueryString with boolean false', () => { + const path = buildQueryString('/data', { active: false }) + expect(path).toEqual('/data?active=false') + }) + + test('buildQueryString with empty params', () => { + const path = buildQueryString('/data', {}) + expect(path).toEqual('/data?') + }) }) }) diff --git a/tests/unit/lib/stats.spec.js b/tests/unit/lib/stats.spec.js index 2e1bbdce00..9c3eb18dc4 100644 --- a/tests/unit/lib/stats.spec.js +++ b/tests/unit/lib/stats.spec.js @@ -1,7 +1,8 @@ import { computeStats, getChartData, - getChartColors + getChartColors, + getPercentage } from '@/lib/stats' const taskMap = new Map(Object.entries({ @@ -175,4 +176,11 @@ describe('lib/stats', () => { data = getChartColors(expectedStatResult, 'all', 'all') expect(data).toEqual(['red', 'blue']) }) + + it('getPercentage', () => { + expect(getPercentage(50, 100)).toEqual('50.00') + expect(getPercentage(1, 3)).toEqual('33.33') + expect(getPercentage(0, 0)).toEqual('0.00') + expect(getPercentage(0, 100)).toEqual('0.00') + }) }) diff --git a/tests/unit/lib/video.spec.js b/tests/unit/lib/video.spec.js index c468a4419d..07a0399f60 100644 --- a/tests/unit/lib/video.spec.js +++ b/tests/unit/lib/video.spec.js @@ -1,6 +1,9 @@ import { + ceilToFrame, + floorToFrame, formatFrame, formatTime, + formatToTimecode, frameToSeconds, roundToFrame } from '@/lib/video' @@ -53,4 +56,28 @@ describe('video', () => { expect(formatTime(60.018, 25)).toBe('00:01:00:00') expect(formatTime(362.018, 25)).toBe('00:06:02:00') }) + + it('ceilToFrame', () => { + expect(ceilToFrame(1, 24)).toBeCloseTo(1.0008, 4) + expect(ceilToFrame(0.95, 24)).toBeCloseTo(0.9591, 4) + expect(ceilToFrame(49.12, 25)).toBeCloseTo(49.1201, 4) + }) + + it('floorToFrame', () => { + expect(floorToFrame(1, 24)).toBeCloseTo(0.9591, 4) + expect(floorToFrame(49.16, 25)).toBeCloseTo(49.16, 4) + }) + + it('formatToTimecode', () => { + expect(formatToTimecode(0, 25)).toBe('00:00:00:00') + expect(formatToTimecode(25, 25)).toBe('00:00:01:00') + expect(formatToTimecode(50, 25)).toBe('00:00:02:00') + expect(formatToTimecode(1525, 25)).toBe('00:01:01:00') + expect(formatToTimecode(null, 25)).toBe('00:00:00:00') + expect(formatToTimecode(-5, 25)).toBe('00:00:00:00') + }) + + it('formatTime with negative time', () => { + expect(formatTime(-1, 25)).toBe('00:00:00:00') + }) }) From b86e825cee7dae268259ce2f349b070212e44919 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 09:42:54 +0100 Subject: [PATCH 09/92] [qa] run linter --- src/lib/csv.js | 6 +++-- src/lib/render.js | 5 ++-- src/lib/sorting.js | 64 +++++++++++++++++++++++++++++----------------- src/lib/video.js | 8 ++++-- 4 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/lib/csv.js b/src/lib/csv.js index 826c303b46..3ea6903181 100644 --- a/src/lib/csv.js +++ b/src/lib/csv.js @@ -266,8 +266,10 @@ const csv = { const entryIds = getStatsEntryIds(mainStats, entryMap) entryIds.forEach(entryId => { - const taskStatusIds = getStatsTaskStatusIdsForEntry(mainStats, entryId) - .filter(s => !excludeStatuses.includes(s)) + const taskStatusIds = getStatsTaskStatusIdsForEntry( + mainStats, + entryId + ).filter(s => !excludeStatuses.includes(s)) const total = getStatsTotalCount( mainStats, taskStatusIds, diff --git a/src/lib/render.js b/src/lib/render.js index 201743d85e..18f56d248f 100644 --- a/src/lib/render.js +++ b/src/lib/render.js @@ -97,9 +97,8 @@ export const renderComment = ( .sort((a, b) => b.length - a.length) .map(escapeRegex) .join('|') - html = html.replace( - new RegExp(pattern, 'g'), - match => replacements.get(match) + html = html.replace(new RegExp(pattern, 'g'), match => + replacements.get(match) ) } diff --git a/src/lib/sorting.js b/src/lib/sorting.js index 8cd9586d07..749d250a7d 100644 --- a/src/lib/sorting.js +++ b/src/lib/sorting.js @@ -264,7 +264,13 @@ export const sortValidationColumns = ( const compareByName = (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }) -const sortEntityResult = (result, sorting, taskMap, thenBySteps, defaultSort) => { +const sortEntityResult = ( + result, + sorting, + taskMap, + thenBySteps, + defaultSort +) => { if (sorting && sorting.length > 0) { const sortInfo = sorting[0] const sortEntities = @@ -283,45 +289,57 @@ const sortEntityResult = (result, sorting, taskMap, thenBySteps, defaultSort) => } export const sortAssetResult = (result, sorting, taskTypeMap, taskMap) => { - return sortEntityResult(result, sorting, taskMap, [ - (a, b) => - a.asset_type_name.localeCompare(b.asset_type_name, undefined, { - numeric: true - }), - compareByName - ], sortAssets) + return sortEntityResult( + result, + sorting, + taskMap, + [ + (a, b) => + a.asset_type_name.localeCompare(b.asset_type_name, undefined, { + numeric: true + }), + compareByName + ], + sortAssets + ) } export const sortShotResult = (result, sorting, taskTypeMap, taskMap) => { - return sortEntityResult(result, sorting, taskMap, [ - sortByEpisode, - (a, b) => - a.sequence_name.localeCompare(b.sequence_name, undefined, { - numeric: true - }), - compareByName - ], sortShots) + return sortEntityResult( + result, + sorting, + taskMap, + [ + sortByEpisode, + (a, b) => + a.sequence_name.localeCompare(b.sequence_name, undefined, { + numeric: true + }), + compareByName + ], + sortShots + ) } export const sortSequenceResult = (result, sorting, taskTypeMap, taskMap) => { return sortEntityResult( - result, sorting, taskMap, + result, + sorting, + taskMap, [sortByEpisode, compareByName], sortByName ) } export const sortEpisodeResult = (result, sorting, taskTypeMap, taskMap) => { - return sortEntityResult( - result, sorting, taskMap, - [compareByName], - sortByName - ) + return sortEntityResult(result, sorting, taskMap, [compareByName], sortByName) } export const sortEditResult = (result, sorting, taskTypeMap, taskMap) => { return sortEntityResult( - result, sorting, taskMap, + result, + sorting, + taskMap, [sortByEpisode, compareByName], sortEdits ) diff --git a/src/lib/video.js b/src/lib/video.js index 2e89a2bf62..236e09ff8c 100644 --- a/src/lib/video.js +++ b/src/lib/video.js @@ -16,13 +16,17 @@ export const roundToFrame = (time, fps) => { export const ceilToFrame = (time, fps) => { const frameFactor = roundPrecision(1 / fps) const frameNumber = Math.ceil(time / frameFactor) - return Math.ceil(frameNumber * frameFactor * PRECISION_FACTOR) / PRECISION_FACTOR + return ( + Math.ceil(frameNumber * frameFactor * PRECISION_FACTOR) / PRECISION_FACTOR + ) } export const floorToFrame = (time, fps) => { const frameFactor = roundPrecision(1 / fps) const frameNumber = Math.floor(time / frameFactor) - return Math.floor(frameNumber * frameFactor * PRECISION_FACTOR) / PRECISION_FACTOR + return ( + Math.floor(frameNumber * frameFactor * PRECISION_FACTOR) / PRECISION_FACTOR + ) } /* From 396fce07491e27a71b5515739fc2877e2ffc2af0 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 09:50:55 +0100 Subject: [PATCH 10/92] [qa] Remove e2e tests not used --- tests/e2e/integration/production.js | 16 ---------------- tests/e2e/plugins/index.js | 3 --- tests/e2e/support/index.js | 3 --- 3 files changed, 22 deletions(-) delete mode 100644 tests/e2e/integration/production.js delete mode 100644 tests/e2e/plugins/index.js delete mode 100644 tests/e2e/support/index.js diff --git a/tests/e2e/integration/production.js b/tests/e2e/integration/production.js deleted file mode 100644 index 8c86e846d2..0000000000 --- a/tests/e2e/integration/production.js +++ /dev/null @@ -1,16 +0,0 @@ -describe('Production creation', () => { - beforeEach(() => { - cy.request('POST', '/api/auth/login', { - email: 'admin@example.com', - password: 'mysecretpassword' - }) - .its('body') - .as('currentUser') - }) - - it('sets auth cookie when logging in via form submission', function () { - cy.visit('/open-productions') - cy.get('#create-production-button').click() - cy.get('.modal.is-active').should('contain', 'Add') - }) -}) diff --git a/tests/e2e/plugins/index.js b/tests/e2e/plugins/index.js deleted file mode 100644 index 894e0c87cd..0000000000 --- a/tests/e2e/plugins/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (on, config) { - // configure plugins here -} diff --git a/tests/e2e/support/index.js b/tests/e2e/support/index.js deleted file mode 100644 index 894e0c87cd..0000000000 --- a/tests/e2e/support/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function (on, config) { - // configure plugins here -} From 2002c61568c118c4b5aa7e5d9f7e79330181a310 Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 09:51:49 +0100 Subject: [PATCH 11/92] [tests] fix ESLint errors across test files Remove unused variables and parameters, convert promise callbacks to async/await, fix useless escapes, and add eslint-disable comments for browser globals in test environment. Co-Authored-By: Claude Opus 4.6 --- tests/archive/assets.spec.js | 4 +-- tests/setup.js | 1 + tests/unit/lib/drafts.spec.js | 1 + tests/unit/lib/errors.spec.js | 1 + tests/unit/lib/func.spec.js | 26 ++++++++----------- tests/unit/lib/lang.spec.js | 4 +-- tests/unit/lib/render.spec.js | 4 +-- tests/unit/lib/sorting.spec.js | 9 +++---- tests/unit/modals/buildfiltermodal.spec.js | 26 +++++++------------ tests/unit/modals/shothistorymodal.spec.js | 18 +++++-------- tests/unit/store/productions.spec.js | 30 +++++++++++----------- 11 files changed, 53 insertions(+), 71 deletions(-) diff --git a/tests/archive/assets.spec.js b/tests/archive/assets.spec.js index 205b676448..4e8b8ba81d 100644 --- a/tests/archive/assets.spec.js +++ b/tests/archive/assets.spec.js @@ -450,7 +450,7 @@ describe('Actions', () => { let mockCommit = vi.fn() peopleApi.createFilter = vi.fn( - (listType, name, query, productionId, entityType) => { + (listType, name, query) => { return Promise.resolve(query) } ) @@ -479,7 +479,7 @@ describe('Actions', () => { const mockCommit = vi.fn() peopleApi.removeFilter = vi.fn( - (searchQuery) => { + () => { return Promise.resolve() } ) diff --git a/tests/setup.js b/tests/setup.js index e5ad9b1890..a8cb3617e4 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,2 +1,3 @@ +/* eslint-disable no-undef */ const noop = () => {} Object.defineProperty(window, 'scrollTo', { value: noop, writable: true }) diff --git a/tests/unit/lib/drafts.spec.js b/tests/unit/lib/drafts.spec.js index 9659a0d91b..219bca7730 100644 --- a/tests/unit/lib/drafts.spec.js +++ b/tests/unit/lib/drafts.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ import { describe, it, expect, vi, beforeEach } from 'vitest' import drafts from '@/lib/drafts' diff --git a/tests/unit/lib/errors.spec.js b/tests/unit/lib/errors.spec.js index b490207be6..30901aeeb2 100644 --- a/tests/unit/lib/errors.spec.js +++ b/tests/unit/lib/errors.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable no-undef */ import { describe, it, expect, vi, beforeEach } from 'vitest' import errors from '@/lib/errors' diff --git a/tests/unit/lib/func.spec.js b/tests/unit/lib/func.spec.js index d8b17cf3f5..5578149dd2 100644 --- a/tests/unit/lib/func.spec.js +++ b/tests/unit/lib/func.spec.js @@ -1,18 +1,15 @@ import func from '@/lib/func' describe('func', () => { - test('runPromiseMapAsSeries', () => new Promise((done) => { + test('runPromiseMapAsSeries', async () => { let counter = 0 const mapFunc = (item) => { counter += item return Promise.resolve(item) } - func.runPromiseMapAsSeries([1, 5], mapFunc) - .then(() => { - expect(counter).toEqual(6) - done() - }) - })) + await func.runPromiseMapAsSeries([1, 5], mapFunc) + expect(counter).toEqual(6) + }) test('throttle', () => { let count = 0 @@ -23,16 +20,13 @@ describe('func', () => { expect(count).toBe(1) // throttled }) - test('runPromiseAsSeries', () => new Promise((done) => { + test('runPromiseAsSeries', async () => { let counter = 0 const promises = [ - Promise.resolve().then(() => { counter += 1 }), - Promise.resolve().then(() => { counter += 5 }) + Promise.resolve().then(() => { counter += 1; return undefined }), + Promise.resolve().then(() => { counter += 5; return undefined }) ] - func.runPromiseAsSeries(promises) - .then(() => { - expect(counter).toEqual(6) - done() - }) - })) + await func.runPromiseAsSeries(promises) + expect(counter).toEqual(6) + }) }) diff --git a/tests/unit/lib/lang.spec.js b/tests/unit/lib/lang.spec.js index 0704fb1f78..728f15a9a4 100644 --- a/tests/unit/lib/lang.spec.js +++ b/tests/unit/lib/lang.spec.js @@ -6,7 +6,7 @@ import i18n from '@/lib/i18n' import store from '@/store' class ColorHash { - constructor (colorData) { + constructor () { } hex (str) { @@ -14,7 +14,7 @@ class ColorHash { } } -global.ColorHash = ColorHash +globalThis.ColorHash = ColorHash describe('lang', () => { store.commit('USER_LOGIN', { diff --git a/tests/unit/lib/render.spec.js b/tests/unit/lib/render.spec.js index 3a004f79b5..c3a7bfe0cc 100644 --- a/tests/unit/lib/render.spec.js +++ b/tests/unit/lib/render.spec.js @@ -61,11 +61,11 @@ describe('render', () => { 'Text **bold** ' let result = renderMarkdown(input) expect(result.trim()).toEqual( - '

Text bold

') + '

Text bold

') input = 'Text **bold** ' result = renderMarkdown(input) expect(result.trim()).toEqual( - '

Text bold

') + '

Text bold

') }) test('renderFileSize', () => { diff --git a/tests/unit/lib/sorting.spec.js b/tests/unit/lib/sorting.spec.js index 4155121964..afe96281e8 100644 --- a/tests/unit/lib/sorting.spec.js +++ b/tests/unit/lib/sorting.spec.js @@ -549,10 +549,8 @@ describe('lib/sorting', () => { const taskMap = new Map() taskMap.set('task1', { task_status_short_name: 'status A' }) taskMap.set('task2', { task_status_short_name: 'status B' }) - const resultsMetadata = - sortAssetResult(entries, sortingMetadata, taskTypeMap, taskMap) - const resultsTaskTypes = - sortAssetResult(entries, sortingTaskType, taskTypeMap, taskMap) + sortAssetResult(entries, sortingMetadata, taskTypeMap, taskMap) + sortAssetResult(entries, sortingTaskType, taskTypeMap, taskMap) /* expect(resultsMetadata).toHaveLength(5) expect(resultsMetadata[0].id).toEqual(5) @@ -637,8 +635,7 @@ describe('lib/sorting', () => { ['task1', { task_status_short_name: 'status A' }], ['task2', { task_status_short_name: 'status B' }] ]) - const resultsMetadata = - sortShotResult(entries, sortingMetadata, taskTypeMap, taskMap) + sortShotResult(entries, sortingMetadata, taskTypeMap, taskMap) const resultsTaskTypes = sortShotResult(entries, sortingTaskType, taskTypeMap, taskMap) /* diff --git a/tests/unit/modals/buildfiltermodal.spec.js b/tests/unit/modals/buildfiltermodal.spec.js index 00d09cdd6a..51d5256d34 100644 --- a/tests/unit/modals/buildfiltermodal.spec.js +++ b/tests/unit/modals/buildfiltermodal.spec.js @@ -16,7 +16,6 @@ const router = createRouter({ describe('BuildFilterModal', () => { let store, assetStore, peopleStore, shotStore, taskStore let wrapper - let getters beforeEach(() => { assetStore = { @@ -54,7 +53,7 @@ describe('BuildFilterModal', () => { } }, actions: { - changeSearch({ commit, state }, query) { + changeSearch({ commit }, query) { commit('CHANGE_SEARCH', query) } } @@ -135,7 +134,6 @@ describe('BuildFilterModal', () => { wrapper = shallowMount(BuildFilterModal, { store, - getters, i18n, router, global: { @@ -154,8 +152,7 @@ describe('BuildFilterModal', () => { wrapper.findComponent(BuildFilterModal) }) describe('mount with query', () => { - it('task types', () => - new Promise(done => { + it('task types', async () => { expect(wrapper.find('.task-type-filter').exists()).toBeFalsy() wrapper.setData({ taskTypeFilters: { @@ -168,13 +165,10 @@ describe('BuildFilterModal', () => { ] } }) - wrapper.vm.$nextTick().then(() => { - expect(wrapper.find('.task-type-filter').exists()).toBeTruthy() - done() - }) - })) - it('descriptors', () => - new Promise(done => { + await wrapper.vm.$nextTick() + expect(wrapper.find('.task-type-filter').exists()).toBeTruthy() + }) + it('descriptors', async () => { expect(wrapper.find('.descriptor-filter').exists()).toBeFalsy() wrapper.setData({ metadataDescriptorFilters: { @@ -188,11 +182,9 @@ describe('BuildFilterModal', () => { ] } }) - wrapper.vm.$nextTick().then(() => { - expect(wrapper.find('.descriptor-filter').exists()).toBeTruthy() - done() - }) - })) + await wrapper.vm.$nextTick() + expect(wrapper.find('.descriptor-filter').exists()).toBeTruthy() + }) }) }) diff --git a/tests/unit/modals/shothistorymodal.spec.js b/tests/unit/modals/shothistorymodal.spec.js index 5fd0c7b2c4..bdd66a5744 100644 --- a/tests/unit/modals/shothistorymodal.spec.js +++ b/tests/unit/modals/shothistorymodal.spec.js @@ -12,7 +12,6 @@ import peopleStoreFixture from '../fixtures/person-store' describe('ShotHistoryModal', () => { let store, shotStore let wrapper - let getters beforeEach(() => { shotStore = { @@ -51,7 +50,6 @@ describe('ShotHistoryModal', () => { wrapper = shallowMount(ShotHistoryModal, { store, - getters, i18n, global: { mocks: { @@ -72,16 +70,14 @@ describe('ShotHistoryModal', () => { expect(tableInfo.props().isLoading).toBe(false) expect(modal.findAll('.shot-version')).toHaveLength(0) }) - it('spinner on loading', () => new Promise(done => { + it('spinner on loading', async () => { wrapper.setData({ isLoading: true }) - nextTick(() => { - const modal = wrapper.findComponent(ShotHistoryModal) - const tableInfo = wrapper.findComponent(TableInfo) - expect(tableInfo.props().isLoading).toBe(true) - expect(modal.findAll('.shot-version')).toHaveLength(0) - done() - }) - })) + await nextTick() + const modal = wrapper.findComponent(ShotHistoryModal) + const tableInfo = wrapper.findComponent(TableInfo) + expect(tableInfo.props().isLoading).toBe(true) + expect(modal.findAll('.shot-version')).toHaveLength(0) + }) it('data loaded', async () => { await wrapper.vm.loadData() const modal = wrapper.findComponent(ShotHistoryModal) diff --git a/tests/unit/store/productions.spec.js b/tests/unit/store/productions.spec.js index a4b84cdddd..04cdae764e 100644 --- a/tests/unit/store/productions.spec.js +++ b/tests/unit/store/productions.spec.js @@ -310,14 +310,14 @@ describe('Productions store', () => { })) } let mockCommit = vi.fn() - productionApi.getProduction = vi.fn(productionId => Promise.resolve({ id: '1' })) + productionApi.getProduction = vi.fn(() => Promise.resolve({ id: '1' })) await store.actions.loadProduction({ commit: mockCommit, state }, 'production-id') expect(productionApi.getProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(1) expect(mockCommit).toHaveBeenNthCalledWith(1, UPDATE_PRODUCTION, { id: '1' }) mockCommit = vi.fn() - productionApi.getProduction = vi.fn(productionId => Promise.resolve({ id: '5' })) + productionApi.getProduction = vi.fn(() => Promise.resolve({ id: '5' })) await store.actions.loadProduction({ commit: mockCommit, state }, 'production-id') expect(productionApi.getProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(1) @@ -325,7 +325,7 @@ describe('Productions store', () => { /* mockCommit = vi.fn() - productionApi.getProduction = vi.fn(productionId => Promise.reject(new Error('error'))) + productionApi.getProduction = vi.fn(() => Promise.reject(new Error('error'))) await store.actions.loadProduction({ commit: mockCommit, state }, 'production-id') expect(productionApi.getProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(0) @@ -334,17 +334,17 @@ describe('Productions store', () => { test('newProduction', async () => { let mockCommit = vi.fn() - productionApi.newProduction = vi.fn(productionId => Promise.resolve({ id: '1' })) + productionApi.newProduction = vi.fn(() => Promise.resolve({ id: '1' })) await store.actions.newProduction({ commit: mockCommit, state: null }, 'production-id') expect(productionApi.newProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(1) expect(mockCommit).toHaveBeenNthCalledWith(1, ADD_PRODUCTION, { id: '1' }) mockCommit = vi.fn() - productionApi.newProduction = vi.fn(productionId => Promise.reject(new Error('error'))) + productionApi.newProduction = vi.fn(() => Promise.reject(new Error('error'))) try { await store.actions.newProduction({ commit: mockCommit, state: null }, 'production-id') - } catch (e) { + } catch { expect(productionApi.newProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(0) } @@ -352,17 +352,17 @@ describe('Productions store', () => { test('editProduction', async () => { let mockCommit = vi.fn() - productionApi.updateProduction = vi.fn(productionId => Promise.resolve({ id: '1' })) + productionApi.updateProduction = vi.fn(() => Promise.resolve({ id: '1' })) await store.actions.editProduction({ commit: mockCommit, state: null }, 'production-id') expect(productionApi.updateProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(1) expect(mockCommit).toHaveBeenNthCalledWith(1, UPDATE_PRODUCTION, { id: '1' }) mockCommit = vi.fn() - productionApi.updateProduction = vi.fn(productionId => Promise.reject(new Error('error'))) + productionApi.updateProduction = vi.fn(() => Promise.reject(new Error('error'))) try { await store.actions.editProduction({ commit: mockCommit, state: null }, 'production-id') - } catch (e) { + } catch { expect(productionApi.updateProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(0) } @@ -370,17 +370,17 @@ describe('Productions store', () => { test('deleteProduction', async () => { let mockCommit = vi.fn() - productionApi.deleteProduction = vi.fn(productionId => Promise.resolve({ id: '1' })) + productionApi.deleteProduction = vi.fn(() => Promise.resolve({ id: '1' })) await store.actions.deleteProduction({ commit: mockCommit, state: null }, 'production-id') expect(productionApi.deleteProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(1) expect(mockCommit).toHaveBeenNthCalledWith(1, REMOVE_PRODUCTION, 'production-id') mockCommit = vi.fn() - productionApi.deleteProduction = vi.fn(productionId => Promise.reject(new Error('error'))) + productionApi.deleteProduction = vi.fn(() => Promise.reject(new Error('error'))) try { await store.actions.deleteProduction({ commit: mockCommit, state: null }, 'production-id') - } catch (e) { + } catch { expect(productionApi.deleteProduction).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(0) } @@ -437,7 +437,7 @@ describe('Productions store', () => { productionApi.postAvatar = vi.fn(() => Promise.reject()) try { await store.actions.uploadProductionAvatar({ commit: mockCommit, state }, 'production-id') - } catch (e) { + } catch { expect(productionApi.postAvatar).toBeCalledTimes(1) expect(mockCommit).toBeCalledTimes(0) } @@ -578,7 +578,7 @@ describe('Productions store', () => { } const descriptor = { id: '456', field_name: 'descriptor name', project_id: '1' } - productionApi.getMetadataDescriptor = vi.fn((_, __) => Promise.resolve(descriptor)) + productionApi.getMetadataDescriptor = vi.fn(() => Promise.resolve(descriptor)) await store.actions.refreshMetadataDescriptor({ commit: mockCommit, state }, descriptor.id) expect(productionApi.getMetadataDescriptor).toBeCalledTimes(1) expect(productionApi.getMetadataDescriptor).toHaveBeenNthCalledWith(1, '123', descriptor.id) @@ -587,7 +587,7 @@ describe('Productions store', () => { descriptor.id = '789' mockCommit = vi.fn() - productionApi.getMetadataDescriptor = vi.fn((_, __) => Promise.resolve(descriptor)) + productionApi.getMetadataDescriptor = vi.fn(() => Promise.resolve(descriptor)) await store.actions.refreshMetadataDescriptor({ commit: mockCommit, state }, descriptor.id) expect(productionApi.getMetadataDescriptor).toBeCalledTimes(1) expect(productionApi.getMetadataDescriptor).toHaveBeenNthCalledWith(1, '123', descriptor.id) From a59b65e56d910e565fd8992f7c01f930c876915d Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 09:52:06 +0100 Subject: [PATCH 12/92] [qa] Add tests linting commands --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index ced8a48fdd..ab062ed6fe 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "build": "vite build", "format": "prettier --write --list-different src/**/*.{js,vue}", "lint": "eslint src/", + "lint:test": "eslint tests/", "lint:fix": "npm run lint -- --fix", + "lint:test:fix": "npm run lint:test -- --fix", "preview": "vite preview", "test": "npm run test:unit", "test:unit": "vitest tests/unit", From dbb2172e8d0b84fbe6299abffdaadc2245cfa73d Mon Sep 17 00:00:00 2001 From: Frank Rousseau Date: Thu, 5 Mar 2026 11:10:12 +0100 Subject: [PATCH 13/92] [widgets] convert AddComment to composition API Co-Authored-By: Claude Opus 4.6 --- src/components/widgets/AddComment.vue | 1167 ++++++++++++------------- 1 file changed, 554 insertions(+), 613 deletions(-) diff --git a/src/components/widgets/AddComment.vue b/src/components/widgets/AddComment.vue index 9eaa3a513e..afd666ce6d 100644 --- a/src/components/widgets/AddComment.vue +++ b/src/components/widgets/AddComment.vue @@ -1,6 +1,6 @@