New test: Add location (#2222)

* New test: Add location
 - Add script to download some test files to allows tests to have a stable path for creating locations
 - Improve onboarding test
 - Add custom Cypress commands for fastOnboarding and deleteLibrary
 - Add some documentation related to tests to CONTRIBUTING.md
 - Update some deps

* Download small test-data for Cypress CI

* Replace globstar with find to make it compatible with bash 3.x

* Fix react too many re-renders
 - add-location test should now pass successfully

* Make job model task text match less flaky

* Check if we were redirected to onboarding after Library was deleted
 - Check if sidebar entry was created after adding a location
 - Put route regex's into a separate file
 - Make onboarding test ensure that no Library exists before running

* Enable test retries
 - Log cypress default config during test run

* Run tests under webkit
 - Pass CI envvar to tests to ensure correct cypress config

* Attempt fix CI config again

* Remove single use checkUrlIsLibrary function
This commit is contained in:
Vítor Vasconcellos 2024-03-23 18:24:16 -03:00 committed by GitHub
parent 904f210fc1
commit e5b57aa0ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 491 additions and 170 deletions

View file

@ -72,11 +72,15 @@ jobs:
runTests: false
working-directory: .
- name: Download test data
run: pnpm test-data small
- name: E2E test
uses: cypress-io/github-action@v6
with:
build: npx cypress info
install: false
command: pnpm test:e2e
command: env CI=true pnpm test:e2e
working-directory: apps/web
- uses: actions/upload-artifact@v4

2
.gitignore vendored
View file

@ -83,3 +83,5 @@ spacedrive
.github/scripts/deps
.vite-inspect
vite.config.ts.*
/test-data

View file

@ -58,6 +58,13 @@ To quickly run only the desktop app after `prep`, you can use:
Also, the react-devtools can be launched using `pnpm dlx react-devtools`.
However, it must be executed before starting the desktop app for it to connect.
You can download a bundle with sample files to test the app by running:
- `pnpm test-data`
Only for Linux and macOS (Requires curl and tar).
The test files will be located in a directory called `test-data` in the root of the spacedrive repo.
To run the web app:
- `pnpm dev:web`
@ -68,15 +75,23 @@ You can launch these individually if you'd prefer:
- `cargo run -p sd-server` (server)
- `pnpm web dev` (web interface)
To run the e2e tests for the web app:
- `pnpm web test:e2e`
If you are developing a new test, you can execute Cypress in interactive mode with:
- `pnpm web test:interactive`
To run the landing page:
- `pnpm landing dev`
If you encounter any issues, ensure that you are using the following versions of Rust, Node and Pnpm:
- Rust version: **1.75.0**
- Node version: **18.17**
- Pnpm version: **8.0.0**
- Rust version: **1.75**
- Node version: **18.18**
- Pnpm version: **8.15**
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.

View file

@ -56,7 +56,7 @@
"@octokit/openapi-types": "^20.0.0",
"@sd/config": "workspace:*",
"@svgr/webpack": "^8.1.0",
"@types/node": ">18.x",
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-burger-menu": "^2.8.7",
"@types/react-dom": "^18.2.22",

View file

@ -4,5 +4,5 @@ module.exports = {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
},
ignorePatterns: ['playwright.config.ts', 'tests/**/*', 'cypress/**/*']
ignorePatterns: ['playwright.config.ts', 'tests/**/*', 'cypress/**/*', 'cypress.config.ts']
};

View file

@ -1,5 +1,11 @@
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { inspect } from 'node:util';
import { defineConfig } from 'cypress';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ci_specific = {
// Double all the default timeouts
// https://docs.cypress.io/guides/references/configuration#Timeouts
@ -8,16 +14,28 @@ const ci_specific = {
taskTimeout: 60000 * 2,
pageLoadTimeout: 60000 * 2,
requestTimeout: 5000 * 2,
responseTimeout: 30000 * 2
responseTimeout: 30000 * 2,
// Enable test retries
// https://docs.cypress.io/guides/guides/test-retries#Configure-retry-attempts-for-all-modes
retries: 2
};
export default defineConfig({
const config = defineConfig({
e2e: {
baseUrl: 'http://localhost:8002',
setupNodeEvents(on, config) {
// implement node event listeners here
setupNodeEvents(on) {
on('task', {
repoRoot: () => {
return resolve(join(__dirname, '../../'));
}
});
}
},
video: true,
experimentalWebKitSupport: true,
...(process.env.CI === 'true' ? ci_specific : {})
});
console.log('Cypress default config:', inspect(config, { depth: null, colors: true }));
export default config;

View file

@ -1,4 +1,5 @@
import { discord, libraryName } from '../fixtures/onboarding.json';
import { discordUrl, libraryName, privacyUrl } from '../fixtures/onboarding.json';
import { libraryRegex, onboardingRegex } from '../fixtures/routes';
describe('Onboarding', () => {
// TODO: Create debug flag to bypass auto language detection
@ -9,8 +10,13 @@ describe('Onboarding', () => {
}
});
// Delete previous library if it exists
cy.url()
.should('match', new RegExp(`${libraryRegex.source}|${onboardingRegex.source}`))
.then((url) => (onboardingRegex.test(url) ? url : cy.deleteLibrary(libraryName)));
// Check redirect to initial alpha onboarding screen
cy.url().should('contain', '/onboarding/alpha');
cy.url().should('match', /\/onboarding\/alpha$/);
// Check application name is present
cy.get('h1').should('contain', 'Spacedrive');
@ -26,7 +32,7 @@ describe('Onboarding', () => {
// Check Join Discord button exists and point to a valid discord invite
cy.get('button').contains('Join Discord').click();
cy.get('@winOpen').should('be.calledWith', discord);
cy.get('@winOpen').should('be.calledWith', discordUrl);
// Check we have a button to continue to the Library creation
cy.get('a')
@ -35,7 +41,7 @@ describe('Onboarding', () => {
.click();
// Check we were redirect to Library creation screen
cy.url().should('contain', '/onboarding/new-library');
cy.url().should('match', /\/onboarding\/new-library$/);
// Check create library screen title
cy.get('h2').should('contain', 'Create a Library');
@ -61,10 +67,10 @@ describe('Onboarding', () => {
cy.get('@libraryNameInput').type(libraryName);
// Check we have a button to continue to the add default locations screen
cy.get('button').contains('New library').click();
cy.get('@newLibraryButton').click();
// Check redirect to add default locations
cy.url().should('contain', '/onboarding/locations');
cy.url().should('match', /\/onboarding\/locations$/);
// Check we have a Toggle All button
cy.get('#toggle-all').as('toggleAllButton');
@ -107,7 +113,7 @@ describe('Onboarding', () => {
cy.get('button').contains('Continue').click();
// Check redirect to privacy screen
cy.url().should('contain', '/onboarding/privacy');
cy.url().should('match', /\/onboarding\/privacy$/);
// Check privacy screen title
cy.get('h2').should('contain', 'Your Privacy');
@ -122,56 +128,22 @@ describe('Onboarding', () => {
// Check More info button exists and point to the valid pravacy policy
cy.get('button').contains('More info').click();
cy.get('@winOpen').should(
'be.calledWith',
'https://www.spacedrive.com/docs/product/resources/privacy'
);
cy.get('@winOpen').should('be.calledWith', privacyUrl);
// Check we have a button to finish onboarding
cy.get('button[type="submit"]').contains('Continue').click();
// Check redirect to privacy screen
cy.url().should('contain', '/onboarding/creating-library');
cy.url().should('match', /\/onboarding\/creating-library$/);
// FIX-ME: This fails a lot, due to the creating library screen only being show for a short time
// Check creating library screen title
// cy.get('h2').should('contain', 'Creating your library');
// Check redirect to Library
cy.url().should((url) => {
expect(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\//.test(url)).to
.be.true;
});
cy.checkUrlIsLibrary();
// Click on the library submenu
cy.get('button[aria-haspopup="menu"]').contains(libraryName).click();
cy.get('a').contains('Manage Library').click();
// Check redirect to Library settings
cy.url().should('contain', '/settings/library/general');
// Check Library seetings screen title
cy.get('h1').should('contain', 'Library Settings');
// Check Library name is correct
cy.get('label')
.contains('Name')
.parent()
.find('input')
.should((input) => {
expect(input.val()).to.eq(libraryName);
});
// Delete Library
cy.get('button').contains('Delete').click();
// Check confirmation modal for deleting appears
cy.get('body > div[role="dialog"]').as('deleteModal');
// Check modal title
cy.get('@deleteModal').find('h2').should('contain', 'Delete Library');
// Confirm delete
cy.get('@deleteModal').find('button').contains('Delete').click();
// Delete library
cy.deleteLibrary(libraryName);
});
});

View file

@ -0,0 +1,88 @@
import { locationName } from '../fixtures/location.json';
import { libraryName } from '../fixtures/onboarding.json';
import { locationRegex } from '../fixtures/routes';
describe('Location', () => {
before(() => {
cy.fastOnboarding(libraryName);
});
it('Add Location', () => {
// Click on the "Add Location" button
cy.get('button').contains('Add Location').click();
// Get the input field for the new location inside modal
cy.get('h2')
.contains('New location')
.parent()
.find('input[name="path"]')
.as('newLocationInput');
// Get the form for the new location inside modal
cy.get('@newLocationInput')
.should('have.value', '')
.then(($input) =>
cy.window().then((win) => {
const input = $input[0];
if (input == null || !(input instanceof win.HTMLInputElement)) {
throw new Error('Input not found');
}
return input.form;
})
)
.as('newLocationForm');
cy.get('@newLocationForm').within(() => {
// Check if the "Open new location once added" checkbox is checked by default
cy.get('label')
.contains('Open new location once added')
.parent()
.find('input[type="checkbox"]')
.should('be.checked');
// Check if the "Add" button is disabled
cy.get('button').contains('Add').as('addLocationButton');
cy.get('@addLocationButton').should('be.disabled');
// Check if the "Add" button is enabled after typing a valid location
cy.get('@newLocationInput').type('/');
cy.get('@addLocationButton').should('be.enabled');
// Check if the "Add" button goes back to disabled after clearing the input
cy.get('@newLocationInput').clear();
cy.get('@addLocationButton').should('be.disabled');
// Check if the "Add" is disabled and an error message is shown after typing an invalid location
cy.get('@newLocationInput').type('nonExisting/path/I/hope');
cy.get('@addLocationButton').should('be.disabled');
cy.get('div > p').contains("location not found <path='nonExisting/path/I/hope'>");
// Get location and add it as a new location
cy.task<string>('repoRoot').then((repoRoot) => {
cy.get('@newLocationInput').clear().type(`${repoRoot}/${locationName}`);
cy.get('@addLocationButton').click();
});
});
// Check if location was added to sidebar
cy.get('div.group').children('div:contains("Locations") + a').contains(locationName);
// Check if location is being scanned
cy.get('button[id="job-manager-button"]').click();
cy.get('span')
.contains('Recent Jobs')
.parent()
.parent()
.within(() =>
cy
.get('p')
.invoke('text')
.should('match', new RegExp(`^(Adding|Added) location "${locationName}"$`))
.should('exist')
);
// Check redirect to location root page
cy.url().should('match', locationRegex);
});
});

View file

@ -0,0 +1,3 @@
{
"locationName": "test-data"
}

View file

@ -1,4 +1,5 @@
{
"discord": "https://discord.gg/ukRnWSnAbG",
"discordUrl": "https://discord.gg/ukRnWSnAbG",
"privacyUrl": "https://www.spacedrive.com/docs/product/resources/privacy",
"libraryName": "Test Library"
}

View file

@ -0,0 +1,8 @@
export const onboardingRegex = /\/onboarding\/alpha$/;
export const libraryRegex = /\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\//;
export const newLibraryRegex = /\/onboarding\/new-library$/;
export const onboardingLocationRegex = /\/onboarding\/locations$/;
export const onboardingPrivacyRegex = /\/onboarding\/privacy$/;
export const librarySettingsRegex = /\/settings\/library\/general$/;
export const onboardingLibraryRegex = /\/onboarding\/new-library$/;
export const locationRegex = /\/location\/1$/;

View file

@ -1,37 +1,97 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
import {
libraryRegex,
librarySettingsRegex,
onboardingLibraryRegex,
onboardingLocationRegex,
onboardingPrivacyRegex,
onboardingRegex
} from '../fixtures/routes';
declare global {
namespace Cypress {
interface Chainable {
deleteLibrary(libraryName: string): Chainable<void>;
fastOnboarding(libraryName: string): Chainable<void>;
checkUrlIsLibrary(): Chainable<string>;
}
}
}
Cypress.Commands.add('checkUrlIsLibrary', () =>
cy.url().should((url) => expect(libraryRegex.test(url)).to.be.true)
);
Cypress.Commands.add('fastOnboarding', (libraryName: string) => {
// Initial alpha onboarding screen
cy.visit('/');
// Delete previous library if it exists
cy.url()
.should('match', new RegExp(`${libraryRegex.source}|${onboardingRegex.source}`))
.then((url) => (onboardingRegex.test(url) ? url : cy.deleteLibrary(libraryName)));
cy.get('a').contains('Continue').should('have.attr', 'href', '/onboarding/new-library').click();
// Library name screen
cy.url().should('match', onboardingLibraryRegex);
cy.get('input[placeholder="e.g. \\"James\' Library\\""]').type(libraryName);
cy.get('button').contains('New library').click();
// Default locations screen
cy.url().should('match', onboardingLocationRegex);
cy.get('button').contains('Continue').click();
// Privacy screen
cy.url().should('match', onboardingPrivacyRegex);
cy.get('button[type="submit"]').contains('Continue').click();
// Check redirect to Library
cy.checkUrlIsLibrary();
});
Cypress.Commands.add('deleteLibrary', (libraryName: string) => {
// Click on the library submenu
cy.get('button[aria-haspopup="menu"]').contains(libraryName).click();
cy.get('a').contains('Manage Library').click();
// Check redirect to Library settings
cy.url().should('match', librarySettingsRegex);
// Check Library seetings screen title
cy.get('h1').should('contain', 'Library Settings');
// Check Library name is correct
cy.get('label')
.contains('Name')
.parent()
.find('input')
.should((input) => {
expect(input.val()).to.eq(libraryName);
});
// Delete Library
cy.get('button').contains('Delete').click();
// Check confirmation modal for deleting appears
cy.get('body > div[role="dialog"]').as('deleteModal');
// Check modal title
cy.get('@deleteModal').find('h2').should('contain', 'Delete Library');
cy.on('uncaught:exception', (err, runnable) => {
// These errors are expected to occour right after the Library is deleted
if (err.message.includes('Attempted to do library operation with no library set')) {
return false;
}
});
// Confirm delete
cy.get('@deleteModal').find('button').contains('Delete').click();
// After deleting a library check we are redirected back to onboarding);
cy.url().should('match', onboardingRegex);
});
export {};

View file

@ -3,7 +3,7 @@
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"],
"resolveJsonModule": true,
"resolveJsonModule": true
},
"include": ["**/*.ts"]
}

View file

@ -4,13 +4,14 @@
"type": "module",
"scripts": {
"dev:api": "env E2E_TEST=1 cargo run -p sd-server",
"dev:web": "env WAIT_ON_TIMEOUT=1800000 start-test dev:api http://localhost:8080 dev",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "VITE_SD_DEMO_MODE=true playwright test",
"test:e2e": "env WAIT_ON_TIMEOUT=1800000 start-test dev:api http://localhost:8080 dev http://localhost:8002 cy:run",
"test:interactive": "env WAIT_ON_TIMEOUT=1800000 start-test dev:api http://localhost:8080 dev http://localhost:8002 cy:open",
"cy:run": "env ELECTRON_EXTRA_LAUNCH_ARGS=--lang=en cypress run",
"cy:run": "cypress run --browser webkit",
"cy:open": "env ELECTRON_EXTRA_LAUNCH_ARGS=--lang=en cypress open --e2e",
"typecheck": "tsc -b",
"lint": "eslint src --cache"
@ -35,6 +36,7 @@
"autoprefixer": "^10.4.18",
"cypress": "^13.7.0",
"eslint-plugin-cypress": "^2.15.1",
"playwright-webkit": "^1.42.1",
"postcss": "^8.4.36",
"rollup-plugin-visualizer": "^5.12.0",
"start-server-and-test": "^2.0.3",

View file

@ -5,7 +5,7 @@
"declarationDir": "dist",
"types": ["vite/client"]
},
"include": ["src", "src/demoData.json"],
"include": ["src", "src/demoData.json", ".eslintrc.cjs"],
"references": [
{
"path": "../../interface"

View file

@ -61,24 +61,25 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
// Can stay here until we add columns view
// Once added, the provided parent related logic should move to useExplorerDroppable
// that way we don't have to re-use the same logic for each view
const { parent } = explorer;
const { setDroppableRef } = useExplorerDroppable({
...(explorer.parent?.type === 'Location' && {
...(parent?.type === 'Location' && {
allow: ['Path', 'NonIndexedPath'],
data: { type: 'location', path: path ?? '/', data: explorer.parent.location },
data: { type: 'location', path: path ?? '/', data: parent.location },
disabled:
drag?.type === 'dragging' &&
explorer.parent.location.id === drag.sourceLocationId &&
parent.location.id === drag.sourceLocationId &&
(path ?? '/') === drag.sourcePath
}),
...(explorer.parent?.type === 'Ephemeral' && {
...(parent?.type === 'Ephemeral' && {
allow: ['Path', 'NonIndexedPath'],
data: { type: 'location', path: explorer.parent.path },
disabled: drag?.type === 'dragging' && explorer.parent.path === drag.sourcePath
data: { type: 'location', path: parent.path },
disabled: drag?.type === 'dragging' && parent.path === drag.sourcePath
}),
...(explorer.parent?.type === 'Tag' && {
...(parent?.type === 'Tag' && {
allow: 'Path',
data: { type: 'tag', data: explorer.parent.tag },
disabled: drag?.type === 'dragging' && explorer.parent.tag.id === drag.sourceTagId
data: { type: 'tag', data: parent.tag },
disabled: drag?.type === 'dragging' && parent.tag.id === drag.sourceTagId
})
});

View file

@ -65,6 +65,7 @@ export default () => {
popover={jobManagerPopover}
trigger={
<Button
id="job-manager-button"
size="icon"
variant="subtle"
className="text-sidebar-inkFaint ring-offset-sidebar radix-state-open:bg-sidebar-selected/50"

View file

@ -1,6 +1,6 @@
import clsx from 'clsx';
import { Suspense, useEffect, useMemo, useRef } from 'react';
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { Navigate, Outlet, useNavigate } from 'react-router-dom';
import {
ClientContextProvider,
initPlausible,
@ -31,6 +31,8 @@ import { DndContext } from './DndContext';
import Sidebar from './Sidebar';
const Layout = () => {
useRedirectToNewLocation();
const { libraries, library } = useClientContext();
const os = useOperatingSystem();
const showControls = useShowControls();
@ -40,8 +42,6 @@ const Layout = () => {
const layoutRef = useRef<HTMLDivElement>(null);
useRedirectToNewLocation();
const ctxValue = useMemo(() => ({ ref: layoutRef }), [layoutRef]);
usePlausible();

View file

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useLibraryQuery, useSelector } from '@sd/client';
import { explorerStore } from '~/app/$libraryId/Explorer/store';
@ -29,8 +30,10 @@ export const useRedirectToNewLocation = () => {
(j.completed_task_count > 0 || j.completed_at != null)
);
if (hasIndexerJob) {
navigate(`/${libraryId}/location/${newLocation}`);
explorerStore.newLocationToRedirect = null;
}
useEffect(() => {
if (hasIndexerJob) {
navigate(`/${libraryId}/location/${newLocation}`);
explorerStore.newLocationToRedirect = null;
}
}, [hasIndexerJob, libraryId, newLocation, navigate]);
};

View file

@ -70,12 +70,12 @@
},
"devDependencies": {
"@sd/config": "workspace:*",
"@types/node": ">18.x",
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"@types/uuid": "^9.0.8",
"tailwindcss": "^3.4.1",
"type-fest": "^4.12.0",
"type-fest": "^4.13.0",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"vite-plugin-svgr": "^3.3.0"

View file

@ -22,14 +22,15 @@
"client": "pnpm --filter @sd/client -- ",
"storybook": "pnpm --filter @sd/storybook -- ",
"prisma": "cd core && cargo prisma",
"dev:web": "turbo run dev --filter @sd/web --filter @sd/server",
"dev:web": "pnpm --filter @sd/web dev:web",
"dev:desktop": "pnpm run --filter @sd/desktop tauri dev",
"bootstrap:desktop": "cargo clean && ./scripts/setup.sh && pnpm i && pnpm prep && pnpm tauri dev",
"codegen": "cargo test -p sd-core api::tests::test_and_export_rspc_bindings -- --exact",
"typecheck": "pnpm -r typecheck",
"lint": "turbo run lint",
"lint:fix": "turbo run lint -- --fix",
"clean": "cargo clean; git clean -qfX ."
"clean": "cargo clean; git clean -qfX .",
"test-data": "./scripts/test-data.sh"
},
"pnpm": {
"patchedDependencies": {
@ -37,16 +38,15 @@
"@contentlayer/cli@0.3.4": "patches/@contentlayer__cli@0.3.4.patch"
},
"overrides": {
"@types/node": ">18.x",
"@types/node": ">18.18.x",
"react-router": "=6.20.1",
"react-router-dom": "=6.20.1",
"@remix-run/router": "=1.13.1",
"@contentlayer/cli": "=0.3.4",
"@typescript-eslint/parser": "^7.1.1"
"@contentlayer/cli": "=0.3.4"
}
},
"devDependencies": {
"@babel/plugin-syntax-import-assertions": "^7.23.3",
"@babel/plugin-syntax-import-assertions": "^7.24.1",
"@cspell/dict-rust": "^4.0.2",
"@cspell/dict-typescript": "^3.1.2",
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",

View file

@ -4,7 +4,7 @@ import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from
import { NormalisedCache, useCache, useNodes } from '../cache';
import { LibraryConfigWrapped, Procedures } from '../core';
import { valtioPersist } from '../lib';
import { nonLibraryClient, useBridgeQuery } from '../rspc';
import { useBridgeQuery } from '../rspc';
// The name of the localStorage key for caching library data
const libraryCacheLocalStorageKey = 'sd-library-list2'; // `2` is because the format of this underwent a breaking change when introducing normalised caching
@ -42,7 +42,7 @@ export const useCachedLibraries = () => {
export async function getCachedLibraries(cache: NormalisedCache, client: AlphaClient<Procedures>) {
const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
const libraries = client.query(['library.list']).then(result => {
const libraries = client.query(['library.list']).then((result) => {
cache.withNodes(result.nodes);
const libraries = cache.withCache(result.items);
@ -62,7 +62,6 @@ export async function getCachedLibraries(cache: NormalisedCache, client: AlphaCl
}
}
return await libraries;
}
@ -105,6 +104,7 @@ export const ClientContextProvider = ({
);
};
// million-ignore
export const useClientContext = () => {
const ctx = useContext(ClientContext);

View file

@ -52,7 +52,7 @@
"@storybook/types": "^8.0.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/node": ">18.x",
"@types/node": ">18.18.x",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"autoprefixer": "^10.4.18",

View file

@ -5,12 +5,11 @@ settings:
excludeLinksFromLockfile: false
overrides:
'@types/node': '>18.x'
'@types/node': '>18.18.x'
react-router: '=6.20.1'
react-router-dom: '=6.20.1'
'@remix-run/router': '=1.13.1'
'@contentlayer/cli': '=0.3.4'
'@typescript-eslint/parser': ^7.1.1
patchedDependencies:
'@contentlayer/cli@0.3.4':
@ -25,8 +24,8 @@ importers:
.:
devDependencies:
'@babel/plugin-syntax-import-assertions':
specifier: ^7.23.3
version: 7.23.3(@babel/core@7.24.0)
specifier: ^7.24.1
version: 7.24.1(@babel/core@7.24.0)
'@cspell/dict-rust':
specifier: ^4.0.2
version: 4.0.2
@ -292,7 +291,7 @@ importers:
specifier: ^8.1.0
version: 8.1.0(typescript@5.4.2)
'@types/node':
specifier: '>18.x'
specifier: '>18.18.x'
version: 20.11.29
'@types/react':
specifier: ^18.2.67
@ -641,6 +640,9 @@ importers:
eslint-plugin-cypress:
specifier: ^2.15.1
version: 2.15.1(eslint@8.57.0)
playwright-webkit:
specifier: ^1.42.1
version: 1.42.1
postcss:
specifier: ^8.4.36
version: 8.4.36
@ -841,7 +843,7 @@ importers:
specifier: workspace:*
version: link:../packages/config
'@types/node':
specifier: '>18.x'
specifier: '>18.18.x'
version: 20.11.29
'@types/react':
specifier: ^18.2.67
@ -856,8 +858,8 @@ importers:
specifier: ^3.4.1
version: 3.4.1
type-fest:
specifier: ^4.12.0
version: 4.12.0
specifier: ^4.13.0
version: 4.13.0
typescript:
specifier: ^5.4.2
version: 5.4.2
@ -935,7 +937,7 @@ importers:
specifier: ^7.3.1
version: 7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/parser':
specifier: ^7.1.1
specifier: ^7.3.1
version: 7.3.1(eslint@8.57.0)(typescript@5.4.2)
'@vitejs/plugin-react-swc':
specifier: ^3.6.0
@ -1086,7 +1088,7 @@ importers:
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.4.1)
'@types/node':
specifier: '>18.x'
specifier: '>18.18.x'
version: 20.11.29
'@types/react':
specifier: ^18.2.67
@ -1138,22 +1140,22 @@ importers:
specifier: ^7.24.0
version: 7.24.0
'@babel/eslint-parser':
specifier: ^7.23.10
version: 7.23.10(@babel/core@7.24.0)(eslint@8.57.0)
specifier: ^7.24.1
version: 7.24.1(@babel/core@7.24.0)(eslint@8.57.0)
'@babel/eslint-plugin':
specifier: ^7.23.5
version: 7.23.5(@babel/eslint-parser@7.23.10)(eslint@8.57.0)
version: 7.23.5(@babel/eslint-parser@7.24.1)(eslint@8.57.0)
'@types/mustache':
specifier: ^4.2.5
version: 4.2.5
'@types/node':
specifier: '>18.x'
specifier: '>18.18.x'
version: 20.11.29
'@typescript-eslint/eslint-plugin':
specifier: ^7.3.1
version: 7.3.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/parser':
specifier: ^7.1.1
specifier: ^7.3.1
version: 7.3.1(eslint@8.57.0)(typescript@5.4.2)
eslint:
specifier: ^8.57.0
@ -1586,8 +1588,8 @@ packages:
transitivePeerDependencies:
- supports-color
/@babel/eslint-parser@7.23.10(@babel/core@7.24.0)(eslint@8.57.0):
resolution: {integrity: sha512-3wSYDPZVnhseRnxRJH6ZVTNknBz76AEnyC+AYYhasjP3Yy23qz0ERR7Fcd2SHmYuSFJ2kY9gaaDd3vyqU09eSw==}
/@babel/eslint-parser@7.24.1(@babel/core@7.24.0)(eslint@8.57.0):
resolution: {integrity: sha512-d5guuzMlPeDfZIbpQ8+g1NaCNuAGBBGNECh0HVqz1sjOeVLh2CEaifuOysCH18URW6R7pqXINvf5PaR/dC6jLQ==}
engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
peerDependencies:
'@babel/core': ^7.11.0
@ -1600,14 +1602,14 @@ packages:
semver: 6.3.1
dev: true
/@babel/eslint-plugin@7.23.5(@babel/eslint-parser@7.23.10)(eslint@8.57.0):
/@babel/eslint-plugin@7.23.5(@babel/eslint-parser@7.24.1)(eslint@8.57.0):
resolution: {integrity: sha512-03+E/58Hoo/ui69gR+beFdGpplpoVK0BSIdke2iw4/Bz7eGN0ssRenNlnU4nmbkowNQOPCStKSwFr8H6DiY49g==}
engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0}
peerDependencies:
'@babel/eslint-parser': ^7.11.0
eslint: ^7.5.0 || ^8.0.0
dependencies:
'@babel/eslint-parser': 7.23.10(@babel/core@7.24.0)(eslint@8.57.0)
'@babel/eslint-parser': 7.24.1(@babel/core@7.24.0)(eslint@8.57.0)
eslint: 8.57.0
eslint-rule-composer: 0.3.0
dev: true
@ -2054,8 +2056,8 @@ packages:
'@babel/core': 7.24.0
'@babel/helper-plugin-utils': 7.24.0
/@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.24.0):
resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==}
/@babel/plugin-syntax-import-assertions@7.24.1(@babel/core@7.24.0):
resolution: {integrity: sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@ -2803,7 +2805,7 @@ packages:
'@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.0)
'@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.0)
'@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.0)
'@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.24.0)
'@babel/plugin-syntax-import-assertions': 7.24.1(@babel/core@7.24.0)
'@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.24.0)
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.0)
'@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0)
@ -3087,10 +3089,6 @@ packages:
peerDependencies:
'@effect-ts/otel-node': '*'
peerDependenciesMeta:
'@effect-ts/core':
optional: true
'@effect-ts/otel':
optional: true
'@effect-ts/otel-node':
optional: true
dependencies:
@ -10335,7 +10333,7 @@ packages:
resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
'@typescript-eslint/parser': ^7.1.1
'@typescript-eslint/parser': ^7.0.0
eslint: ^8.56.0
typescript: '*'
peerDependenciesMeta:
@ -10360,6 +10358,27 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 6.21.0
'@typescript-eslint/types': 6.21.0
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.2)
'@typescript-eslint/visitor-keys': 6.21.0
debug: 4.3.4(supports-color@8.1.1)
eslint: 8.57.0
typescript: 5.4.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser@7.3.1(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==}
engines: {node: ^18.18.0 || >=20.0.0}
@ -13937,11 +13956,11 @@ packages:
dependencies:
'@next/eslint-plugin-next': 14.1.3
'@rushstack/eslint-patch': 1.7.2
'@typescript-eslint/parser': 7.3.1(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.34.1(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@ -13970,7 +13989,7 @@ packages:
eslint-plugin-promise: ^6.0.0
dependencies:
eslint: 8.57.0
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)
eslint-plugin-n: 16.6.2(eslint@8.57.0)
eslint-plugin-promise: 6.1.1(eslint@8.57.0)
dev: true
@ -13994,7 +14013,7 @@ packages:
- supports-color
dev: true
/eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0):
/eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0):
resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
@ -14004,8 +14023,8 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@ -14017,7 +14036,37 @@ packages:
- supports-color
dev: true
/eslint-module-utils@2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
/eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: '*'
eslint-import-resolver-node: '*'
eslint-import-resolver-typescript: '*'
eslint-import-resolver-webpack: '*'
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
eslint:
optional: true
eslint-import-resolver-node:
optional: true
eslint-import-resolver-typescript:
optional: true
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.2)
debug: 3.2.7(supports-color@8.1.1)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
dev: true
/eslint-module-utils@2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
engines: {node: '>=4'}
peerDependencies:
@ -14042,7 +14091,6 @@ packages:
debug: 3.2.7(supports-color@8.1.1)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
dev: true
@ -14068,7 +14116,7 @@ packages:
eslint-compat-utils: 0.1.2(eslint@8.57.0)
dev: true
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.3.1)(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
peerDependencies:
@ -14087,7 +14135,7 @@ packages:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.3.1)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@ -20037,6 +20085,15 @@ packages:
hasBin: true
dev: true
/playwright-webkit@1.42.1:
resolution: {integrity: sha512-NKgCwBlmHQZmM5a6q36nUQ1/nIfJLyblioxfiHIrPU4JHUqVrwx5+uWg2xZp010+vTSx4IicscT4hCiPWQx5zA==}
engines: {node: '>=16'}
hasBin: true
requiresBuild: true
dependencies:
playwright-core: 1.42.1
dev: true
/playwright@1.42.1:
resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==}
engines: {node: '>=16'}
@ -23545,8 +23602,8 @@ packages:
resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==}
engines: {node: '>=14.16'}
/type-fest@4.12.0:
resolution: {integrity: sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==}
/type-fest@4.13.0:
resolution: {integrity: sha512-nKO1N9IFeTec3jnNe/3nZlX+RzwZsvT3c4akWC3IlhYGQbRSPFMBe87vmoaymS3hW2l/rs+4ptDDTxzcbqAcmA==}
engines: {node: '>=16'}
dev: true
@ -24339,7 +24396,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
globrex: 0.1.2
tsconfck: 3.0.3(typescript@5.4.2)
vite: 5.1.6(sass@1.72.0)
vite: 5.1.6(@types/node@20.11.29)
transitivePeerDependencies:
- supports-color
- typescript
@ -24350,7 +24407,7 @@ packages:
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>18.x'
'@types/node': '>18.18.x'
less: '*'
lightningcss: ^1.21.0
sass: '*'
@ -24385,7 +24442,7 @@ packages:
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': '>18.x'
'@types/node': '>18.18.x'
less: '*'
lightningcss: ^1.21.0
sass: '*'
@ -24420,7 +24477,7 @@ packages:
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': '>18.x'
'@types/node': '>18.18.x'
less: '*'
lightningcss: ^1.21.0
sass: '*'

View file

@ -26,10 +26,10 @@
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/eslint-parser": "^7.23.10",
"@babel/eslint-parser": "^7.24.1",
"@babel/eslint-plugin": "^7.23.5",
"@types/mustache": "^4.2.5",
"@types/node": ">18.x",
"@types/node": ">18.18.x",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"eslint": "^8.57.0",

86
scripts/test-data.sh Executable file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euEo pipefail
# This script is used to download test data for Spacedrive e2e tests.
_root="$(CDPATH='' cd "$(dirname "$0")/.." && pwd -P)"
_test_data_dir="${_root}/test-data"
# Check if curl and tar are available
if ! command -v curl &>/dev/null; then
echo "curl is required to download test data" >&2
exit 1
fi
if ! command -v tar &>/dev/null; then
echo "tar is required to extract test data" >&2
exit 1
fi
rm -rf "$_test_data_dir"
mkdir "$_test_data_dir"
if [ "${1:-}" == "small" ]; then
echo "Downloading WPT test resources..."
curl -L# 'https://github.com/web-platform-tests/wpt/archive/refs/heads/master.tar.gz' \
| tar -xzf - -C "$_test_data_dir" \
wpt-master/images
else
echo "Downloading WPT test resources..."
curl -L# 'https://github.com/web-platform-tests/wpt/archive/refs/heads/master.tar.gz' \
| tar -xzf - -C "$_test_data_dir" \
wpt-master/images \
wpt-master/jpegxl/resources
echo "Downloading HEIF test resources..."
curl -L# 'https://github.com/nokiatech/heif_conformance/archive/refs/heads/master.tar.gz' \
| tar -xzf - -C "$_test_data_dir" \
heif_conformance-master/conformance_files
echo "Downloading WEBP test resources..."
curl -L# 'https://github.com/webmproject/libwebp-test-data/archive/refs/heads/main.tar.gz' \
| tar -xzf - -C "$_test_data_dir"
echo "Downloading PNG test resources..."
mkdir -p "${_test_data_dir}/png-test-suite"
curl -L# 'http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz' \
| tar -xzf - -C "${_test_data_dir}/png-test-suite"
echo "Downloading image-rs test resources..."
curl -L# 'https://github.com/image-rs/image/archive/refs/heads/main.tar.gz' \
| tar -xzf - -C "$_test_data_dir" \
image-main/tests/images/bmp \
image-main/tests/images/gif \
image-main/tests/images/ico \
image-main/tests/images/tiff
echo "Downloading chromium media test resources..."
mkdir -p "${_test_data_dir}/chromium-media"
curl -L# 'https://chromium.googlesource.com/chromium/src/+archive/refs/heads/main/media/test/data.tar.gz' \
| tar -xzf - -C "${_test_data_dir}/chromium-media"
echo "Downloading chromium pdf test resources..."
mkdir -p "${_test_data_dir}/chromium-pdf"
curl -L# 'https://chromium.googlesource.com/chromium/src/+archive/refs/heads/main/pdf/test/data.tar.gz' \
| tar -xzf - -C "${_test_data_dir}/chromium-pdf"
fi
while IFS= read -r -d '' _test_file; do
_mime_type="$(file -b --mime-type "$_test_file")"
case "$_mime_type" in
image/* | audio/* | video/*)
_type_dir="${_test_data_dir}/${_mime_type%%/*}"
;;
application/pdf)
_type_dir="${_test_data_dir}/pdf"
;;
*)
continue
;;
esac
mkdir -p "$_type_dir"
mv "$_test_file" "$_type_dir"
done < <(find "$_test_data_dir" -type f -print0)
rm -rf "${_test_data_dir}"/{wpt-master,heif_conformance-master,libwebp-test-data-main,png-test-suite,image-main,chromium-media,chromium-pdf}