build: Migrate to Vite 🚀 (#6713)

* init

* add: vite dev build working

* fix: href serving from public

* feat: add ejs plugin

* feat: migrated env files and ejs templating

* chore: add types related to envs

* chore: add vite-env types

* feat: support vite pwa

* chore: upgrade vite pwa

* chore: pin node version to 16.18.1

* chore: preserve use of nodejs 14

* refactor: preserve REACT_APP as env prefix

* chore: support esm environment variables

* fix ts config

* use VITE prefix and remove vite-plugin-env-compatible

* introduce import-meta-loader for building pacakge as webpack isn't compatible with import.meta syntax

* lint

* remove import.meta.env in main.js

* set debug flag to false

* migrate to vitest and use jest-canvas-mock 2.4.0 so its comp
atible with vite

* integrate vitest-ui

* fix most of teh test

* snaps

* Add script for testing with vite ui

* fix all tests related to mocking

* fix more test

* fix more

* fix flip.test.tsx

* fix contentxmenu snaps

* fix regression snaps

* fix excalidraw.test.tsx and this makes all tests finally pass :)

* use node 16

* specify node version

* use node 16 in lint as well

* fix mobile.test.tsx

* use node 16

* add style-loader

* upgrade to node 18

* fix lint package.json

* support eslint with vite

* fix lint

* fix lint

* fix ts

* remove pwa/sw stuff

* use env vars in EJS the vite way

* fix lint

* move remainig jest mock/spy to vite

* don't cache locales

* fix regex

* add fonts cache

* tweak

* add custom service worker

* upgrade vite and create font cache again

* cache fonts.css and locales

* tweak

* use manifestTransforms for filtering locales

* use assets js pattern for locales

* add font.css to globIgnore so its pushed to fonts cache

* create a separate chunk for locales with rollup

* remove manifestTransforms and fix glob pattern for locales to filter from workbox pre-cache

* push sourcemaps in production

* add comments in config

* lint

* use node 18

* disable pwa in dev

* fix

* fix

* increase limit of bundle

* upgrade vite-pwa to latest

* remove public/workbox so workbox assets are not precached

* fon't club en.json and percentages.json with manual locales chunk to fix first load+offline mode

* tweak regex

* remove happy-dom as its not used

* add comment

* use any instead of ts-ignore

* cleanup

* remove jest-canvas-mock resolution as vite-canvas-mock was patched locking deps at 2.4.0

* use same theme color present in entry point

---------

Co-authored-by: Weslley Braga <weslley@bambee.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2023-07-26 15:55:20 +05:30 committed by GitHub
parent 2a2495175b
commit e93bbc5776
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 6012 additions and 9758 deletions

View file

@ -11,7 +11,7 @@ export const trackEvent = (
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
if (typeof window === "undefined" || import.meta.env.VITE_WORKER_ID) {
return;
}

View file

@ -6,7 +6,6 @@ import {
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
ENV,
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
@ -384,7 +383,7 @@ const chartTypeBar = (
y,
groupId,
backgroundColor,
process.env.NODE_ENV === ENV.DEVELOPMENT,
import.meta.env.DEV,
),
];
};
@ -473,7 +472,7 @@ const chartTypeLine = (
y,
groupId,
backgroundColor,
process.env.NODE_ENV === ENV.DEVELOPMENT,
import.meta.env.DEV,
),
line,
...lines,

View file

@ -4,8 +4,9 @@ import { reseed } from "../random";
import { render, queryByTestId } from "../tests/test-utils";
import ExcalidrawApp from "../excalidraw-app";
import { vi } from "vitest";
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
describe("Test <App/>", () => {
beforeEach(async () => {

View file

@ -255,6 +255,7 @@ import {
isTransparent,
easeToValuesRAF,
muteFSAbortError,
isTestEnv,
easeOut,
} from "../utils";
import {
@ -1595,10 +1596,7 @@ class App extends React.Component<AppProps, AppState> {
this.excalidrawContainerValue.container =
this.excalidrawContainerRef.current;
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
const setState = this.setState.bind(this);
Object.defineProperties(window.h, {
state: {
@ -1636,7 +1634,7 @@ class App extends React.Component<AppProps, AppState> {
// bounding rects don't work in tests so updating
// the state on init would result in making the test enviro run
// in mobile breakpoint (0 width/height), making everything fail
process.env.NODE_ENV !== "test"
!isTestEnv()
) {
this.refreshDeviceState(this.excalidrawContainerRef.current);
}
@ -8162,10 +8160,7 @@ declare global {
}
}
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
window.h = window.h || ({} as Window["h"]);
Object.defineProperties(window.h, {

View file

@ -16,7 +16,7 @@ const LibraryMenuBrowseButton = ({
return (
<a
className="library-menu-browse-button"
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
href={`${import.meta.env.VITE_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary

View file

@ -319,7 +319,7 @@ const PublishLibrary = ({
formData.append("twitterHandle", libraryData.twitterHandle);
formData.append("website", libraryData.website);
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
fetch(`${import.meta.env.VITE_APP_LIBRARY_BACKEND}/submit`, {
method: "post",
body: formData,
})

View file

@ -10,6 +10,7 @@ import {
waitFor,
withExcalidrawDimensions,
} from "../../tests/test-utils";
import { vi } from "vitest";
export const assertSidebarDockButton = async <T extends boolean>(
hasDockButton: T,
@ -205,7 +206,7 @@ describe("Sidebar", () => {
});
it("<Sidebar.Header> should render close button", async () => {
const onStateChange = jest.fn();
const onStateChange = vi.fn();
const CustomExcalidraw = () => {
return (
<Excalidraw

View file

@ -53,7 +53,7 @@ export const SidebarInner = forwardRef(
}: SidebarProps & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">,
ref: React.ForwardedRef<HTMLDivElement>,
) => {
if (process.env.NODE_ENV === "development" && onDock && docked == null) {
if (import.meta.env.DEV && onDock && docked == null) {
console.warn(
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
);

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
exports[`Test <App/> > should show error modal when using brave and measureText API is not working 1`] = `
<div
data-testid="brave-measure-text-error"
>

View file

@ -443,7 +443,7 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
// we're not cloning non-array & non-plain-object objects because we
// don't support them on excalidraw elements yet. If we do, we need to make
// sure we start cloning them, so let's warn about it.
if (process.env.NODE_ENV === "development") {
if (import.meta.env.DEV) {
if (
objectType !== "[object Object]" &&
objectType !== "[object Array]" &&

View file

@ -1,19 +1,32 @@
import { vi } from "vitest";
import { getPerfectElementSize } from "./sizeHelpers";
import * as constants from "../constants";
const EPSILON_DIGITS = 3;
// Needed so that we can mock the value of constants which is done in
// below tests. In Jest this wasn't needed as global override was possible
// but vite doesn't allow that hence we need to mock
vi.mock(
"../constants.ts",
//@ts-ignore
async (importOriginal) => {
const module: any = await importOriginal();
return { ...module };
},
);
describe("getPerfectElementSize", () => {
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
const { height, width } = getPerfectElementSize("line", 149, 10);
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
});
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
const { height, width } = getPerfectElementSize("line", 10, 140);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
});
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
const { height, width } = getPerfectElementSize("arrow", 200, 20);
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
@ -24,16 +37,19 @@ describe("getPerfectElementSize", () => {
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
});
it("should return adjust height to be width * tan(locked angle)", () => {
const { height, width } = getPerfectElementSize("arrow", 120, 185);
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
});
it("should return height equals to width if locked angle is 45 deg", () => {
const { height, width } = getPerfectElementSize("arrow", 135, 145);
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
});
it("should return height:0 and width:0 when width and height are 0", () => {
const { height, width } = getPerfectElementSize("arrow", 0, 0);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);

View file

@ -955,7 +955,7 @@ describe("textWysiwyg", () => {
// should center align horizontally and vertically by default
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
85,
4.5,
]
@ -979,7 +979,7 @@ describe("textWysiwyg", () => {
// should left align horizontally and bottom vertically after resize
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
15,
65,
]
@ -1001,7 +1001,7 @@ describe("textWysiwyg", () => {
// should right align horizontally and top vertically after resize
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
375,
-539,
]
@ -1279,7 +1279,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Left"));
fireEvent.click(screen.getByTitle("Align top"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
15,
25,
]
@ -1290,7 +1290,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center"));
fireEvent.click(screen.getByTitle("Align top"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
30,
25,
]
@ -1302,7 +1302,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Align top"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
45,
25,
]
@ -1313,7 +1313,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center vertically"));
fireEvent.click(screen.getByTitle("Left"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
15,
45,
]
@ -1325,7 +1325,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center vertically"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
30,
45,
]
@ -1337,7 +1337,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center vertically"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
45,
45,
]
@ -1349,7 +1349,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Align bottom"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
15,
65,
]
@ -1360,7 +1360,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center"));
fireEvent.click(screen.getByTitle("Align bottom"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
30,
65,
]
@ -1371,7 +1371,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Right"));
fireEvent.click(screen.getByTitle("Align bottom"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
[
45,
65,
]

View file

@ -171,10 +171,7 @@ class Collab extends PureComponent<Props, CollabState> {
appJotaiStore.set(collabAPIAtom, collabAPI);
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
window.collab = window.collab || ({} as Window["collab"]);
Object.defineProperties(window, {
collab: {
@ -860,10 +857,7 @@ declare global {
}
}
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
window.collab = window.collab || ({} as Window["collab"]);
}

View file

@ -19,7 +19,9 @@ export const AppWelcomeScreen: React.FC<{
return (
<a
style={{ pointerEvents: "all" }}
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
href={`${
import.meta.env.VITE_APP_PLUS_APP
}?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenSignedInUser`}
key={idx}
>
Excalidraw+

View file

@ -6,7 +6,9 @@ export const ExcalidrawPlusAppLink = () => {
}
return (
<a
href={`${process.env.REACT_APP_PLUS_APP}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
href={`${
import.meta.env.VITE_APP_PLUS_APP
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
target="_blank"
rel="noreferrer"
className="plus-button"

View file

@ -21,10 +21,12 @@ import { ResolutionType } from "../../utility-types";
let FIREBASE_CONFIG: Record<string, any>;
try {
FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
FIREBASE_CONFIG = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
} catch (error: any) {
console.warn(
`Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
`Error JSON parsing firebase config. Supplied value: ${
import.meta.env.VITE_APP_FIREBASE_CONFIG
}`,
);
FIREBASE_CONFIG = {};
}

View file

@ -47,8 +47,8 @@ export const getSyncableElements = (elements: readonly ExcalidrawElement[]) =>
isSyncableElement(element),
) as SyncableExcalidrawElement[];
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
const BACKEND_V2_GET = import.meta.env.VITE_APP_BACKEND_V2_GET_URL;
const BACKEND_V2_POST = import.meta.env.VITE_APP_BACKEND_V2_POST_URL;
const generateRoomId = async () => {
const buffer = new Uint8Array(ROOM_ID_BYTES);
@ -67,16 +67,16 @@ export const getCollabServer = async (): Promise<{
url: string;
polling: boolean;
}> => {
if (process.env.REACT_APP_WS_SERVER_URL) {
if (import.meta.env.VITE_APP_WS_SERVER_URL) {
return {
url: process.env.REACT_APP_WS_SERVER_URL,
url: import.meta.env.VITE_APP_WS_SERVER_URL,
polling: true,
};
}
try {
const resp = await fetch(
`${process.env.REACT_APP_PORTAL_URL}/collab-server`,
`${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`,
);
return await resp.json();
} catch (error) {

View file

@ -1,31 +0,0 @@
import { register as registerServiceWorker } from "../serviceWorkerRegistration";
import { EVENT } from "../constants";
// On Apple mobile devices add the proprietary app icon and splashscreen markup.
// No one should have to do this manually, and eventually this annoyance will
// go away once https://bugs.webkit.org/show_bug.cgi?id=183937 is fixed.
if (
/\b(iPad|iPhone|iPod|Safari)\b/.test(navigator.userAgent) &&
!matchMedia("(display-mode: standalone)").matches
) {
import(/* webpackChunkName: "pwacompat" */ "pwacompat");
}
registerServiceWorker({
onUpdate: (registration) => {
const waitingServiceWorker = registration.waiting;
if (waitingServiceWorker) {
waitingServiceWorker.addEventListener(
EVENT.STATE_CHANGE,
(event: Event) => {
const target = event.target as ServiceWorker;
const state = target.state as ServiceWorkerState;
if (state === "activated") {
window.location.reload();
}
},
);
waitingServiceWorker.postMessage({ type: "SKIP_WAITING" });
}
},
});

View file

@ -7,7 +7,7 @@ const SentryEnvHostnameMap: { [key: string]: string } = {
};
const REACT_APP_DISABLE_SENTRY =
process.env.REACT_APP_DISABLE_SENTRY === "true";
import.meta.env.VITE_APP_DISABLE_SENTRY === "true";
// Disable Sentry locally or inside the Docker to avoid noise/respect privacy
const onlineEnv =
@ -21,7 +21,7 @@ Sentry.init({
? "https://7bfc596a5bf945eda6b660d3015a5460@sentry.io/5179260"
: undefined,
environment: onlineEnv ? SentryEnvHostnameMap[onlineEnv] : undefined,
release: process.env.REACT_APP_GIT_SHA,
release: import.meta.env.VITE_APP_GIT_SHA,
ignoreErrors: [
"undefined is not an object (evaluating 'window.__pad.performLoop')", // Only happens on Safari, but spams our servers. Doesn't break anything
],

10
src/global.d.ts vendored
View file

@ -38,16 +38,6 @@ interface CanvasRenderingContext2D {
) => void;
}
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts
declare namespace NodeJS {
interface ProcessEnv {
readonly REACT_APP_BACKEND_V2_GET_URL: string;
readonly REACT_APP_BACKEND_V2_POST_URL: string;
readonly REACT_APP_PORTAL_URL: string;
readonly REACT_APP_FIREBASE_CONFIG: string;
}
}
interface Clipboard extends EventTarget {
write(data: any[]): Promise<void>;
}

View file

@ -1,6 +1,5 @@
import fallbackLangData from "./locales/en.json";
import percentages from "./locales/percentages.json";
import { ENV } from "./constants";
import { jotaiScope, jotaiStore } from "./jotai";
import { atom, useAtomValue } from "jotai";
import { NestedKeyOf } from "./utility-types";
@ -74,7 +73,7 @@ export const languages: Language[] = [
];
const TEST_LANG_CODE = "__test__";
if (process.env.NODE_ENV === ENV.DEVELOPMENT) {
if (import.meta.env.DEV) {
languages.unshift(
{ code: TEST_LANG_CODE, label: "test language" },
{
@ -145,7 +144,7 @@ export const t = (
if (translation === undefined) {
const errorMessage = `Can't find translation for ${path}`;
// in production, don't blow up the app on a missing translation key
if (process.env.NODE_ENV === "production") {
if (import.meta.env.PROD) {
console.warn(errorMessage);
return "";
}

View file

@ -2,9 +2,8 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import ExcalidrawApp from "./excalidraw-app";
import "./excalidraw-app/pwa";
import "./excalidraw-app/sentry";
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement);
root.render(

View file

@ -9,9 +9,9 @@ const parseEnvVariables = (filepath) => {
},
{},
);
envVars.PKG_NAME = JSON.stringify(pkg.name);
envVars.PKG_VERSION = JSON.stringify(pkg.version);
envVars.IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
envVars.VITE_PKG_NAME = JSON.stringify(pkg.name);
envVars.VITE_PKG_VERSION = JSON.stringify(pkg.version);
envVars.VITE_IS_EXCALIDRAW_NPM_PACKAGE = JSON.stringify(true);
return envVars;
};

View file

@ -59,6 +59,7 @@
"cross-env": "7.0.3",
"css-loader": "6.7.1",
"dotenv": "16.0.1",
"import-meta-loader": "1.1.0",
"mini-css-extract-plugin": "2.6.1",
"postcss-loader": "7.0.1",
"sass-loader": "13.0.2",

View file

@ -4,5 +4,5 @@ if (process.env.NODE_ENV !== ENV.TEST) {
/* global __webpack_public_path__:writable */
__webpack_public_path__ =
window.EXCALIDRAW_ASSET_PATH ||
`https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}/dist/`;
`https://unpkg.com/${process.env.VITE_PKG_NAME}@${process.env.VITE_PKG_VERSION}/dist/`;
}

View file

@ -47,6 +47,9 @@ module.exports = {
exclude:
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
use: [
{
loader: "import-meta-loader",
},
{
loader: "ts-loader",
options: {

View file

@ -50,6 +50,9 @@ module.exports = {
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
use: [
{
loader: "import-meta-loader",
},
{
loader: "ts-loader",
options: {

View file

@ -2916,6 +2916,11 @@ import-local@^3.0.2:
pkg-dir "^4.2.0"
resolve-cwd "^3.0.0"
import-meta-loader@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/import-meta-loader/-/import-meta-loader-1.1.0.tgz#927060305f2d0f88b495f2754aa33387ca6579d7"
integrity sha512-f96r2o8xT+b2KVlOY4x+1KTJmJiapZlf77j1WebR8NQgMG1dpdqijjGl4i/2jMoXch2CVqcQoTMfh5BR7bR8wA==
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"

View file

@ -915,7 +915,7 @@ const drawElementFromCanvas = (
);
if (
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
import.meta.env.VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX ===
"true" &&
hasBoundTextElement(element)
) {

View file

@ -133,12 +133,13 @@ export const exportToSvg = async (
}
let assetPath = "https://excalidraw.com/";
// Asset path needs to be determined only when using package
if (process.env.IS_EXCALIDRAW_NPM_PACKAGE) {
if (import.meta.env.VITE_IS_EXCALIDRAW_NPM_PACKAGE) {
assetPath =
window.EXCALIDRAW_ASSET_PATH ||
`https://unpkg.com/${process.env.PKG_NAME}@${process.env.PKG_VERSION}`;
`https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${
import.meta.env.PKG_VERSION
}`;
if (assetPath?.startsWith("/")) {
assetPath = assetPath.replace("/", `${window.location.origin}/`);

View file

@ -1,147 +0,0 @@
/// <reference lib="webworker" />
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from "workbox-core";
import { ExpirationPlugin } from "workbox-expiration";
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { CacheFirst, StaleWhileRevalidate } from "workbox-strategies";
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache assets generated by your build process.
//
// Their URLs are injected into the __WB_MANIFEST during build (by workbox).
//
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA.
//
// We don't want to precache i18n files so we filter them out
// (normally this should be configured in a webpack workbox plugin, but we don't
// have access to it in CRA) — this is because all users will use at most
// one or two languages, so there's no point fetching all of them. (They'll
// be cached as you load them.)
const manifest = self.__WB_MANIFEST.filter((entry) => {
return !/locales\/[\w-]+json/.test(
typeof entry === "string" ? entry : entry.url,
);
});
precacheAndRoute(manifest);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developer.chrome.com/docs/workbox/app-shell-model/
//
// below is copied verbatim from CRA@5
const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip.
if (request.mode !== "navigate") {
return false;
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith("/_")) {
return false;
}
// If this looks like a URL for a resource, because it contains
// a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
// Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(`${process.env.PUBLIC_URL}/index.html`),
);
// Cache resources that aren't being precached
// -----------------------------------------------------------------------------
registerRoute(
new RegExp("/fonts.css"),
new StaleWhileRevalidate({
cacheName: "fonts",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
}),
);
// since we serve fonts from, don't forget to append new ?v= param when
// updating fonts (glyphs) without changing the filename
registerRoute(
new RegExp("/.+.(ttf|woff2|otf)"),
new CacheFirst({
cacheName: "fonts",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({
maxEntries: 50,
// 90 days
maxAgeSeconds: 7776000000,
}),
],
}),
);
registerRoute(
new RegExp("/locales\\/[\\w-]+json"),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new CacheFirst({
cacheName: "locales",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({
maxEntries: 50,
// 30 days
maxAgeSeconds: 2592000000,
}),
],
}),
);
// -----------------------------------------------------------------------------
self.addEventListener("fetch", (event) => {
if (
event.request.method === "POST" &&
event.request.url.endsWith("/web-share-target")
) {
return event.respondWith(
(async () => {
const formData = await event.request.formData();
const file = formData.get("file");
const webShareTargetCache = await caches.open("web-share-target");
await webShareTargetCache.put("shared-file", new Response(file));
return Response.redirect("/?web-share-target", 303);
})(),
);
}
});
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});

View file

@ -1,162 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export const register = (config?: Config) => {
if (
(process.env.NODE_ENV === "production" ||
process.env.REACT_APP_DEV_ENABLE_SW?.toLowerCase() === "true") &&
"serviceWorker" in navigator
) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener("load", () => {
const isWebexLP = window.location.pathname.startsWith("/webex");
if (isWebexLP) {
unregister(() => {
window.location.reload();
});
return false;
}
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.info(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://bit.ly/CRA-PWA",
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
};
const registerValidSW = (swUrl: string, config?: Config) => {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.info(
"New content is available and will be used when all tabs for this page are closed.",
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.info("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error("Error during service worker registration:", error);
});
};
const checkValidServiceWorker = (swUrl: string, config?: Config) => {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(contentType != null && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch((error) => {
console.info(
"No internet connection found. App is running in offline mode.",
error.message,
);
});
};
export const unregister = (callback?: () => void) => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
return registration.unregister();
})
.then(() => {
callback?.();
})
.catch((error) => {
console.error(error.message);
});
}
};

View file

@ -1,19 +1,16 @@
// vitest.setup.ts
import "vitest-canvas-mock";
import "@testing-library/jest-dom";
import "jest-canvas-mock";
import dotenv from "dotenv";
import { vi } from "vitest";
import polyfill from "./polyfill";
require("fake-indexeddb/auto");
polyfill();
// jest doesn't know of .env.development so we need to init it ourselves
dotenv.config({
path: require("path").resolve(__dirname, "../.env.development"),
});
jest.mock("nanoid", () => {
vi.mock("nanoid", () => {
return {
nanoid: jest.fn(() => "test-id"),
nanoid: vi.fn(() => "test-id"),
};
});
// ReactDOM is located inside index.tsx file

View file

@ -11,23 +11,23 @@ describe("Test MobileMenu", () => {
const { h } = window;
const dimensions = { height: 400, width: 800 };
beforeAll(() => {
mockBoundingClientRect(dimensions);
});
beforeEach(async () => {
await render(<ExcalidrawApp />);
//@ts-ignore
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
});
beforeAll(() => {
mockBoundingClientRect(dimensions);
});
afterAll(() => {
restoreOriginalGetBoundingClientRect();
});
it("should set device correctly", () => {
expect(h.app.device).toMatchInlineSnapshot(`
Object {
{
"canDeviceFitSidebar": false,
"isLandscape": true,
"isMobile": true,
@ -39,7 +39,6 @@ describe("Test MobileMenu", () => {
it("should initialize with welcome screen and hide once user interacts", async () => {
expect(document.querySelector(".welcome-screen-center")).toMatchSnapshot();
UI.clickTool("rectangle");
expect(document.querySelector(".welcome-screen-center")).toBeNull();
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test MobileMenu should initialize with welcome screen and hide once user interacts 1`] = `
exports[`Test MobileMenu > should initialize with welcome screen and hide once user interacts 1`] = `
<div
class="welcome-screen-center"
>

View file

@ -1,15 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`tryParseSpreadsheet works for numbers with comma in them 1`] = `
Object {
"spreadsheet": Object {
"labels": Array [
exports[`tryParseSpreadsheet > works for numbers with comma in them 1`] = `
{
"spreadsheet": {
"labels": [
"Week 1",
"Week 2",
"Week 3",
],
"title": "Users",
"values": Array [
"values": [
814,
10301,
4264,

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 1`] = `1`;
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 1`] = `1`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough arrow 2`] = `
Object {
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > arrow 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
@ -11,7 +11,7 @@ Object {
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -19,18 +19,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
30,
50,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -49,16 +49,16 @@ Object {
}
`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 1`] = `1`;
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 1`] = `1`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough diamond 2`] = `
Object {
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > diamond 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -66,7 +66,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -83,16 +83,16 @@ Object {
}
`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 1`] = `1`;
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 1`] = `1`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough ellipse 2`] = `
Object {
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > ellipse 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -100,7 +100,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -117,8 +117,8 @@ Object {
}
`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough line 1`] = `
Object {
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > line 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
@ -126,7 +126,7 @@ Object {
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -134,18 +134,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
30,
50,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -164,16 +164,16 @@ Object {
}
`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 1`] = `1`;
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 1`] = `1`;
exports[`Test dragCreate add element to the scene when pointer dragging long enough rectangle 2`] = `
Object {
exports[`Test dragCreate > add element to the scene when pointer dragging long enough > rectangle 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -181,7 +181,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 337897,

File diff suppressed because one or more lines are too long

View file

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test Linear Elements Test bound text element should match styles for text editor 1`] = `
exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `
<textarea
class="excalidraw-wysiwyg"
data-type="wysiwyg"
dir="auto"
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
tabindex="0"
wrap="off"
/>

View file

@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`duplicate element on move when ALT is clicked rectangle 1`] = `
Object {
exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0_copy",
"isDeleted": false,
@ -15,7 +15,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 401146281,
@ -32,14 +32,14 @@ Object {
}
`;
exports[`duplicate element on move when ALT is clicked rectangle 2`] = `
Object {
exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -47,7 +47,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 337897,
@ -64,14 +64,14 @@ Object {
}
`;
exports[`move element rectangle 1`] = `
Object {
exports[`move element > rectangle 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -79,7 +79,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 337897,
@ -96,19 +96,19 @@ Object {
}
`;
exports[`move element rectangles with binding arrow 1`] = `
Object {
exports[`move element > rectangles with binding arrow 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": Array [
Object {
"boundElements": [
{
"id": "id2",
"type": "arrow",
},
],
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 100,
"id": "id0",
"isDeleted": false,
@ -116,7 +116,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 337897,
@ -133,19 +133,19 @@ Object {
}
`;
exports[`move element rectangles with binding arrow 2`] = `
Object {
exports[`move element > rectangles with binding arrow 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": Array [
Object {
"boundElements": [
{
"id": "id2",
"type": "arrow",
},
],
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 300,
"id": "id1",
"isDeleted": false,
@ -153,7 +153,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 449462985,
@ -170,20 +170,20 @@ Object {
}
`;
exports[`move element rectangles with binding arrow 3`] = `
Object {
exports[`move element > rectangles with binding arrow 3`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"endArrowhead": null,
"endBinding": Object {
"endBinding": {
"elementId": "id1",
"focus": -0.46666666666666673,
"gap": 10,
},
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 81.48231043525051,
"id": "id2",
"isDeleted": false,
@ -191,23 +191,23 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
81,
81.48231043525051,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 401146281,
"startArrowhead": null,
"startBinding": Object {
"startBinding": {
"elementId": "id0",
"focus": -0.6000000000000001,
"gap": 10,

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`multi point mode in linear elements arrow 1`] = `
Object {
exports[`multi point mode in linear elements > arrow 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
@ -9,33 +9,33 @@ Object {
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 110,
"id": "id0",
"isDeleted": false,
"lastCommittedPoint": Array [
"lastCommittedPoint": [
70,
110,
],
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
20,
30,
],
Array [
[
70,
110,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -54,8 +54,8 @@ Object {
}
`;
exports[`multi point mode in linear elements line 1`] = `
Object {
exports[`multi point mode in linear elements > line 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
@ -63,33 +63,33 @@ Object {
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 110,
"id": "id0",
"isDeleted": false,
"lastCommittedPoint": Array [
"lastCommittedPoint": [
70,
110,
],
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
20,
30,
],
Array [
[
70,
110,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`select single element on the scene arrow 1`] = `
Object {
exports[`select single element on the scene > arrow 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
@ -9,7 +9,7 @@ Object {
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -17,18 +17,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
30,
50,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -47,8 +47,8 @@ Object {
}
`;
exports[`select single element on the scene arrow escape 1`] = `
Object {
exports[`select single element on the scene > arrow escape 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
@ -56,7 +56,7 @@ Object {
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -64,18 +64,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
30,
50,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -94,14 +94,14 @@ Object {
}
`;
exports[`select single element on the scene diamond 1`] = `
Object {
exports[`select single element on the scene > diamond 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -109,7 +109,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -126,14 +126,14 @@ Object {
}
`;
exports[`select single element on the scene ellipse 1`] = `
Object {
exports[`select single element on the scene > ellipse 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -141,7 +141,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": 337897,
@ -158,14 +158,14 @@ Object {
}
`;
exports[`select single element on the scene rectangle 1`] = `
Object {
exports[`select single element on the scene > rectangle 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 50,
"id": "id0",
"isDeleted": false,
@ -173,7 +173,7 @@ Object {
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": 337897,

View file

@ -1,4 +1,5 @@
import { queryByTestId, render, waitFor } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app";
import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState";

View file

@ -1,3 +1,4 @@
import { vi } from "vitest";
import ReactDOM from "react-dom";
import {
render,
@ -21,14 +22,14 @@ const { h } = window;
const mouse = new Pointer("mouse");
jest.mock("../keys.ts", () => {
const actual = jest.requireActual("../keys.ts");
vi.mock("../keys.ts", async (importOriginal) => {
const module: any = await importOriginal();
return {
__esmodule: true,
...actual,
...module,
isDarwin: false,
KEYS: {
...actual.KEYS,
...module.KEYS,
CTRL_OR_CMD: "ctrlKey",
},
};

View file

@ -1,3 +1,4 @@
import { vi } from "vitest";
import { render, updateSceneData, waitFor } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app";
import { API } from "./helpers/api";
@ -15,15 +16,18 @@ Object.defineProperty(window, "crypto", {
},
});
jest.mock("../excalidraw-app/data/index.ts", () => ({
__esmodule: true,
...jest.requireActual("../excalidraw-app/data/index.ts"),
getCollabServer: jest.fn(() => ({
url: /* doesn't really matter */ "http://localhost:3002",
})),
}));
vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
const module = (await importActual()) as any;
return {
__esmodule: true,
...module,
getCollabServer: vi.fn(() => ({
url: /* doesn't really matter */ "http://localhost:3002",
})),
};
});
jest.mock("../excalidraw-app/data/firebase.ts", () => {
vi.mock("../excalidraw-app/data/firebase.ts", () => {
const loadFromFirebase = async () => null;
const saveToFirebase = () => {};
const isSavedToFirebase = () => true;
@ -45,15 +49,17 @@ jest.mock("../excalidraw-app/data/firebase.ts", () => {
};
});
jest.mock("socket.io-client", () => {
return () => {
return {
close: () => {},
on: () => {},
once: () => {},
off: () => {},
emit: () => {},
};
vi.mock("socket.io-client", () => {
return {
default: () => {
return {
close: () => {},
on: () => {},
once: () => {},
off: () => {},
emit: () => {},
};
},
};
});

View file

@ -21,6 +21,7 @@ import { copiedStyles } from "../actions/actionStyles";
import { API } from "./helpers/api";
import { setDateTimeForTests } from "../utils";
import { LibraryItem } from "../types";
import { vi } from "vitest";
const checkpoint = (name: string) => {
expect(renderScene.mock.calls.length).toMatchSnapshot(
@ -39,7 +40,7 @@ const mouse = new Pointer("mouse");
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
beforeEach(() => {
localStorage.clear();
renderScene.mockClear();

View file

@ -1,15 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`restoreElements should restore arrow element correctly 1`] = `
Object {
exports[`restoreElements > should restore arrow element correctly 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": Array [],
"boundElements": [],
"endArrowhead": null,
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 100,
"id": "id-arrow01",
"isDeleted": false,
@ -17,18 +17,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
100,
100,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": Any<Number>,
@ -47,14 +47,14 @@ Object {
}
`;
exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 1`] = `
Object {
exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 1`] = `
{
"angle": 0,
"backgroundColor": "blue",
"boundElements": Array [],
"boundElements": [],
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": Array [
"groupIds": [
"1",
"2",
"3",
@ -66,7 +66,7 @@ Object {
"locked": false,
"opacity": 10,
"roughness": 2,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": Any<Number>,
@ -83,14 +83,14 @@ Object {
}
`;
exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 2`] = `
Object {
exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 2`] = `
{
"angle": 0,
"backgroundColor": "blue",
"boundElements": Array [],
"boundElements": [],
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": Array [
"groupIds": [
"1",
"2",
"3",
@ -102,7 +102,7 @@ Object {
"locked": false,
"opacity": 10,
"roughness": 2,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": Any<Number>,
@ -119,14 +119,14 @@ Object {
}
`;
exports[`restoreElements should restore correctly with rectangle, ellipse and diamond elements 3`] = `
Object {
exports[`restoreElements > should restore correctly with rectangle, ellipse and diamond elements 3`] = `
{
"angle": 0,
"backgroundColor": "blue",
"boundElements": Array [],
"boundElements": [],
"fillStyle": "cross-hatch",
"frameId": null,
"groupIds": Array [
"groupIds": [
"1",
"2",
"3",
@ -138,7 +138,7 @@ Object {
"locked": false,
"opacity": 10,
"roughness": 2,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": Any<Number>,
@ -155,14 +155,14 @@ Object {
}
`;
exports[`restoreElements should restore freedraw element correctly 1`] = `
Object {
exports[`restoreElements > should restore freedraw element correctly 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": Array [],
"boundElements": [],
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 0,
"id": "id-freedraw01",
"isDeleted": false,
@ -170,10 +170,10 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [],
"pressures": Array [],
"points": [],
"pressures": [],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": Any<Number>,
@ -191,16 +191,16 @@ Object {
}
`;
exports[`restoreElements should restore line and draw elements correctly 1`] = `
Object {
exports[`restoreElements > should restore line and draw elements correctly 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": Array [],
"boundElements": [],
"endArrowhead": null,
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 100,
"id": "id-line01",
"isDeleted": false,
@ -208,18 +208,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
100,
100,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": Any<Number>,
@ -238,16 +238,16 @@ Object {
}
`;
exports[`restoreElements should restore line and draw elements correctly 2`] = `
Object {
exports[`restoreElements > should restore line and draw elements correctly 2`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"boundElements": Array [],
"boundElements": [],
"endArrowhead": null,
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 100,
"id": "id-draw01",
"isDeleted": false,
@ -255,18 +255,18 @@ Object {
"link": null,
"locked": false,
"opacity": 100,
"points": Array [
Array [
"points": [
[
0,
0,
],
Array [
[
100,
100,
],
],
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 2,
},
"seed": Any<Number>,
@ -285,18 +285,18 @@ Object {
}
`;
exports[`restoreElements should restore text element correctly passing value for each attribute 1`] = `
Object {
exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"baseline": 0,
"boundElements": Array [],
"boundElements": [],
"containerId": null,
"fillStyle": "hachure",
"fontFamily": 1,
"fontSize": 14,
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 100,
"id": "id-text01",
"isDeleted": false,
@ -306,7 +306,7 @@ Object {
"opacity": 100,
"originalText": "text",
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": Any<Number>,
@ -326,18 +326,18 @@ Object {
}
`;
exports[`restoreElements should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
Object {
exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
{
"angle": 0,
"backgroundColor": "transparent",
"baseline": 0,
"boundElements": Array [],
"boundElements": [],
"containerId": null,
"fillStyle": "hachure",
"fontFamily": 1,
"fontSize": 10,
"frameId": null,
"groupIds": Array [],
"groupIds": [],
"height": 100,
"id": "id-text01",
"isDeleted": false,
@ -347,7 +347,7 @@ Object {
"opacity": 100,
"originalText": "test",
"roughness": 1,
"roundness": Object {
"roundness": {
"type": 3,
},
"seed": Any<Number>,

View file

@ -12,9 +12,10 @@ import { ImportedDataState } from "../../data/types";
import { NormalizedZoomValue } from "../../types";
import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
import { newElementWith } from "../../element/mutateElement";
import { vi } from "vitest";
describe("restoreElements", () => {
const mockSizeHelper = jest.spyOn(sizeHelpers, "isInvisiblySmallElement");
const mockSizeHelper = vi.spyOn(sizeHelpers, "isInvisiblySmallElement");
beforeEach(() => {
mockSizeHelper.mockReset();
@ -152,7 +153,7 @@ describe("restoreElements", () => {
it("when arrow element has undefined endArrowHead", () => {
const arrowElement = API.createElement({ type: "arrow" });
Object.defineProperty(arrowElement, "endArrowhead", {
get: jest.fn(() => undefined),
get: vi.fn(() => undefined),
});
const restoredElements = restore.restoreElements([arrowElement], null);
@ -205,7 +206,7 @@ describe("restoreElements", () => {
[1, 1],
];
Object.defineProperty(lineElement_0, "points", {
get: jest.fn(() => pointsEl_0),
get: vi.fn(() => pointsEl_0),
});
const pointsEl_1 = [
@ -213,7 +214,7 @@ describe("restoreElements", () => {
[5, 6],
];
Object.defineProperty(lineElement_1, "points", {
get: jest.fn(() => pointsEl_1),
get: vi.fn(() => pointsEl_1),
});
const restoredElements = restore.restoreElements(
@ -440,7 +441,7 @@ describe("restoreAppState", () => {
const stubImportedAppState = getDefaultAppState();
Object.defineProperty(stubImportedAppState, "zoom", {
get: jest.fn(() => null),
get: vi.fn(() => null),
});
const stubLocalAppState = getDefaultAppState();

View file

@ -10,11 +10,12 @@ import {
} from "./test-utils";
import { ExcalidrawLinearElement } from "../element/types";
import { reseed } from "../random";
import { vi } from "vitest";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
beforeEach(() => {
localStorage.clear();
renderScene.mockClear();

View file

@ -2,6 +2,7 @@ import { render } from "./test-utils";
import { API } from "./helpers/api";
import ExcalidrawApp from "../excalidraw-app";
import { vi } from "vitest";
const { h } = window;
@ -97,11 +98,11 @@ const waitForNextAnimationFrame = () => {
describe("fitToContent animated", () => {
beforeEach(() => {
jest.spyOn(window, "requestAnimationFrame");
vi.spyOn(window, "requestAnimationFrame");
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it("should ease scroll the viewport to the selected element", async () => {

View file

@ -20,21 +20,21 @@ import ExcalidrawApp from "../excalidraw-app";
import { mutateElement } from "../element/mutateElement";
import { NormalizedZoomValue } from "../types";
import { ROUNDNESS } from "../constants";
import { vi } from "vitest";
import * as blob from "../data/blob";
const { h } = window;
const mouse = new Pointer("mouse");
jest.mock("../data/blob", () => {
const originalModule = jest.requireActual("../data/blob");
// This needs to fixed in vitest mock, as when importActual used with mock
// the tests hangs - https://github.com/vitest-dev/vitest/issues/546.
// But fortunately spying and mocking the return value of spy works :p
const resizeImageFileSpy = vi.spyOn(blob, "resizeImageFile");
const generateIdFromFileSpy = vi.spyOn(blob, "generateIdFromFile");
resizeImageFileSpy.mockImplementation(async (imageFile: File) => imageFile);
generateIdFromFileSpy.mockImplementation(async () => "fileId" as FileId);
//Prevent Node.js modules errors (document is not defined etc...)
return {
__esModule: true,
...originalModule,
resizeImageFile: (imageFile: File) => imageFile,
generateIdFromFile: () => "fileId" as FileId,
};
});
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -42,7 +42,7 @@ beforeEach(async () => {
mouse.reset();
localStorage.clear();
sessionStorage.clear();
jest.clearAllMocks();
vi.clearAllMocks();
Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas,
@ -732,7 +732,6 @@ describe("image", () => {
it("flips an unrotated image horizontally correctly", async () => {
//paste image
await createImage();
await waitFor(() => {
expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
expect(API.getSelectedElements().length).toBeGreaterThan(0);

View file

@ -1,3 +1,4 @@
import { vi } from "vitest";
import { fireEvent, render, waitFor } from "./test-utils";
import { queryByTestId } from "@testing-library/react";
@ -29,11 +30,15 @@ const mockLibraryFilePromise = new Promise<Blob>(async (resolve, reject) => {
}
});
jest.mock("../data/filesystem.ts", () => ({
__esmodule: true,
...jest.requireActual("../data/filesystem.ts"),
fileOpen: jest.fn(() => mockLibraryFilePromise),
}));
vi.mock("../data/filesystem.ts", async (importOriginal) => {
const module = await importOriginal();
return {
__esmodule: true,
//@ts-ignore
...module,
fileOpen: vi.fn(() => mockLibraryFilePromise),
};
});
describe("library", () => {
beforeEach(async () => {

View file

@ -24,8 +24,9 @@ import {
} from "../element/textElement";
import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { vi } from "vitest";
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
const { h } = window;
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
@ -179,16 +180,16 @@ describe("Test Linear Elements", () => {
expect(renderScene).toHaveBeenCalledTimes(11);
expect(line.points.length).toEqual(3);
expect(line.points).toMatchInlineSnapshot(`
Array [
Array [
[
[
0,
0,
],
Array [
[
70,
50,
],
Array [
[
40,
0,
],
@ -273,16 +274,16 @@ describe("Test Linear Elements", () => {
expect(line.points.length).toEqual(3);
expect(line.points).toMatchInlineSnapshot(`
Array [
Array [
[
[
0,
0,
],
Array [
[
70,
50,
],
Array [
[
40,
0,
],
@ -315,12 +316,12 @@ describe("Test Linear Elements", () => {
expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
Array [
Array [
[
[
55.9697848965255,
47.442326230998205,
],
Array [
[
76.08587175006699,
43.294165939653226,
],
@ -363,12 +364,12 @@ describe("Test Linear Elements", () => {
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
expect(newMidPoints).toMatchInlineSnapshot(`
Array [
Array [
[
[
105.96978489652551,
67.4423262309982,
],
Array [
[
126.08587175006699,
63.294165939653226,
],
@ -412,29 +413,29 @@ describe("Test Linear Elements", () => {
expect((h.elements[0] as ExcalidrawLinearElement).points)
.toMatchInlineSnapshot(`
Array [
Array [
0,
0,
],
Array [
85,
75,
],
Array [
70,
50,
],
Array [
105,
70,
],
Array [
40,
0,
],
]
`);
[
[
0,
0,
],
[
85,
75,
],
[
70,
50,
],
[
105,
70,
],
[
40,
0,
],
]
`);
});
it("should update only the first segment midpoint when its point is dragged", async () => {
@ -558,29 +559,29 @@ describe("Test Linear Elements", () => {
expect((h.elements[0] as ExcalidrawLinearElement).points)
.toMatchInlineSnapshot(`
Array [
Array [
0,
0,
],
Array [
85.96978489652551,
77.4423262309982,
],
Array [
70,
50,
],
Array [
106.08587175006699,
73.29416593965323,
],
Array [
40,
0,
],
]
`);
[
[
0,
0,
],
[
85.96978489652551,
77.4423262309982,
],
[
70,
50,
],
[
106.08587175006699,
73.29416593965323,
],
[
40,
0,
],
]
`);
});
it("should update all the midpoints when its point is dragged", async () => {
@ -606,12 +607,12 @@ describe("Test Linear Elements", () => {
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
expect(newMidPoints).toMatchInlineSnapshot(`
Array [
Array [
[
[
31.884084517616053,
23.13275505472383,
],
Array [
[
77.74792546875662,
44.57840982272327,
],
@ -667,12 +668,12 @@ describe("Test Linear Elements", () => {
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
expect(newMidPoints).toMatchInlineSnapshot(`
Array [
Array [
[
[
55.9697848965255,
47.442326230998205,
],
Array [
[
76.08587175006699,
43.294165939653226,
],
@ -704,12 +705,12 @@ describe("Test Linear Elements", () => {
[dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
);
expect(line.points).toMatchInlineSnapshot(`
Array [
Array [
[
[
0,
0,
],
Array [
[
-60,
-100,
],
@ -768,7 +769,7 @@ describe("Test Linear Elements", () => {
textElement,
);
expect(position).toMatchInlineSnapshot(`
Object {
{
"x": 25,
"y": 10,
}
@ -790,7 +791,7 @@ describe("Test Linear Elements", () => {
textElement,
);
expect(position).toMatchInlineSnapshot(`
Object {
{
"x": 75,
"y": 60,
}
@ -824,7 +825,7 @@ describe("Test Linear Elements", () => {
textElement,
);
expect(position).toMatchInlineSnapshot(`
Object {
{
"x": 85.82201843191861,
"y": 75.63461309860818,
}
@ -939,11 +940,11 @@ describe("Test Linear Elements", () => {
expect(textElement.angle).toBe(0);
expect(getBoundTextElementPosition(arrow, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 75,
"y": 60,
}
`);
{
"x": 75,
"y": 60,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
@ -951,26 +952,26 @@ describe("Test Linear Elements", () => {
`);
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
.toMatchInlineSnapshot(`
Array [
20,
20,
105,
80,
55.45893770831013,
45,
]
`);
[
20,
20,
105,
80,
55.45893770831013,
45,
]
`);
rotate(container, -35, 55);
expect(container.angle).toMatchInlineSnapshot(`1.3988061968364685`);
expect(textElement.angle).toBe(0);
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 21.73926141863671,
"y": 73.31003398390868,
}
`);
{
"x": 21.73926141863671,
"y": 73.31003398390868,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
@ -978,15 +979,15 @@ describe("Test Linear Elements", () => {
`);
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
.toMatchInlineSnapshot(`
Array [
20,
20,
102.41961302274555,
86.49012635273976,
55.45893770831013,
45,
]
`);
[
20,
20,
102.41961302274555,
86.49012635273976,
55.45893770831013,
45,
]
`);
});
it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
@ -1004,11 +1005,11 @@ describe("Test Linear Elements", () => {
expect(container.height).toBe(50);
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 75,
"y": 60,
}
`);
{
"x": 75,
"y": 60,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
@ -1016,33 +1017,33 @@ describe("Test Linear Elements", () => {
`);
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
.toMatchInlineSnapshot(`
Array [
20,
20,
105,
80,
55.45893770831013,
45,
]
`);
[
20,
20,
105,
80,
55.45893770831013,
45,
]
`);
resize(container, "ne", [300, 200]);
expect({ width: container.width, height: container.height })
.toMatchInlineSnapshot(`
Object {
"height": 130,
"width": 367,
}
`);
{
"height": 130,
"width": 367,
}
`);
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 272,
"y": 45,
}
`);
{
"x": 272,
"y": 45,
}
`);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
.toMatchInlineSnapshot(`
"Online whiteboard
@ -1050,15 +1051,15 @@ describe("Test Linear Elements", () => {
`);
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
.toMatchInlineSnapshot(`
Array [
20,
35,
502,
95,
205.9061448421403,
52.5,
]
`);
[
20,
35,
502,
95,
205.9061448421403,
52.5,
]
`);
});
it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
@ -1072,11 +1073,11 @@ describe("Test Linear Elements", () => {
expect(container.width).toBe(40);
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 25,
"y": 10,
}
`);
{
"x": 25,
"y": 10,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
@ -1089,19 +1090,19 @@ describe("Test Linear Elements", () => {
expect({ width: container.width, height: container.height })
.toMatchInlineSnapshot(`
Object {
"height": 130,
"width": 340,
}
`);
{
"height": 130,
"width": 340,
}
`);
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 75,
"y": -5,
}
`);
{
"x": 75,
"y": -5,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made easy"
@ -1154,7 +1155,7 @@ describe("Test Linear Elements", () => {
"Online whiteboard collaboration
made easy"
`);
const handleBindTextResizeSpy = jest.spyOn(
const handleBindTextResizeSpy = vi.spyOn(
textElementUtils,
"handleBindTextResize",
);

View file

@ -12,11 +12,12 @@ import {
} from "../element/types";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { KEYS } from "../keys";
import { vi } from "vitest";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
beforeEach(() => {
localStorage.clear();
renderScene.mockClear();

View file

@ -10,11 +10,12 @@ import * as Renderer from "../renderer/renderScene";
import { KEYS } from "../keys";
import { ExcalidrawLinearElement } from "../element/types";
import { reseed } from "../random";
import { vi } from "vitest";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
beforeEach(() => {
localStorage.clear();
renderScene.mockClear();

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Excalidraw/> <MainMenu/> should render main menu with host menu items if passed from host 1`] = `
exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
<div
class="dropdown-menu"
data-testid="dropdown-menu"
@ -108,7 +108,7 @@ exports[`<Excalidraw/> <MainMenu/> should render main menu with host menu items
</div>
`;
exports[`<Excalidraw/> Test UIOptions prop Test canvasActions should render menu with default items when "UIOPtions" is "undefined" 1`] = `
exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = `
<div
class="dropdown-menu"
data-testid="dropdown-menu"

View file

@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`exportToSvg with default arguments 1`] = `
Object {
exports[`exportToSvg > with default arguments 1`] = `
{
"activeEmbeddable": null,
"activeTool": Object {
"activeTool": {
"customType": null,
"lastActiveTool": null,
"locked": false,
@ -40,7 +40,7 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"frameRendering": {
"clip": true,
"enabled": true,
"name": true,
@ -59,21 +59,21 @@ Object {
"openMenu": null,
"openPopup": null,
"openSidebar": null,
"pasteDialog": Object {
"pasteDialog": {
"data": null,
"shown": false,
},
"penDetected": false,
"penMode": false,
"pendingImageElementId": null,
"previousSelectedElementIds": Object {},
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollX": 0,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": Object {},
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {},
"selectedGroupIds": {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
@ -81,13 +81,13 @@ Object {
"showStats": false,
"showWelcomeScreen": false,
"startBoundElement": null,
"suggestedBindings": Array [],
"suggestedBindings": [],
"theme": "light",
"toast": null,
"viewBackgroundColor": "#ffffff",
"viewModeEnabled": false,
"zenModeEnabled": false,
"zoom": Object {
"zoom": {
"value": 1,
},
}

View file

@ -1,6 +1,6 @@
import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils";
import { Excalidraw, Footer, MainMenu } from "../../packages/excalidraw/index";
import { queryByText, queryByTestId } from "@testing-library/react";
import { queryByText, queryByTestId, screen } from "@testing-library/react";
import { GRID_SIZE, THEME } from "../../constants";
import { t } from "../../i18n";
import { useMemo } from "react";
@ -42,7 +42,7 @@ describe("<Excalidraw/>", () => {
container.getElementsByClassName("disable-zen-mode--visible").length,
).toBe(0);
expect(h.state.zenModeEnabled).toBe(true);
screen.debug();
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 1,
@ -74,7 +74,8 @@ describe("<Excalidraw/>", () => {
</Footer>
</Excalidraw>,
));
expect(container.querySelector(".footer-center")).toMatchInlineSnapshot(`
expect(container.querySelector(".footer-center")).toMatchInlineSnapshot(
`
<div
class="footer-center zen-mode-transition"
>
@ -82,7 +83,8 @@ describe("<Excalidraw/>", () => {
This is a custom footer
</div>
</div>
`);
`,
);
});
describe("Test gridModeEnabled prop", () => {

View file

@ -1,15 +1,13 @@
import * as utils from "../../packages/utils";
import { diagramFactory } from "../fixtures/diagramFixture";
import { vi } from "vitest";
import * as mockedSceneExportUtils from "../../scene/export";
import { MIME_TYPES } from "../../constants";
jest.mock("../../scene/export", () => ({
__esmodule: true,
...jest.requireActual("../../scene/export"),
exportToSvg: jest.fn(),
}));
const exportToSvgSpy = vi.spyOn(mockedSceneExportUtils, "exportToSvg");
describe("exportToCanvas", () => {
describe("exportToCanvas", async () => {
const EXPORT_PADDING = 10;
it("with default arguments", async () => {
@ -32,10 +30,9 @@ describe("exportToCanvas", () => {
});
});
describe("exportToBlob", () => {
describe("exportToBlob", async () => {
describe("mime type", () => {
afterEach(jest.restoreAllMocks);
// afterEach(vi.restoreAllMocks);
it("should change image/jpg to image/jpeg", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),
@ -48,7 +45,6 @@ describe("exportToBlob", () => {
});
expect(blob?.type).toBe(MIME_TYPES.jpg);
});
it("should default to image/png", async () => {
const blob = await utils.exportToBlob({
...diagramFactory(),
@ -57,16 +53,14 @@ describe("exportToBlob", () => {
});
it("should warn when using quality with image/png", async () => {
const consoleSpy = jest
const consoleSpy = vi
.spyOn(console, "warn")
.mockImplementationOnce(() => void 0);
await utils.exportToBlob({
...diagramFactory(),
mimeType: MIME_TYPES.png,
quality: 1,
});
expect(consoleSpy).toHaveBeenCalledWith(
`"quality" will be ignored for "${MIME_TYPES.png}" mimeType`,
);
@ -75,10 +69,12 @@ describe("exportToBlob", () => {
});
describe("exportToSvg", () => {
const mockedExportUtil = mockedSceneExportUtils.exportToSvg as jest.Mock;
const passedElements = () => mockedExportUtil.mock.calls[0][0];
const passedOptions = () => mockedExportUtil.mock.calls[0][1];
afterEach(jest.resetAllMocks);
const passedElements = () => exportToSvgSpy.mock.calls[0][0];
const passedOptions = () => exportToSvgSpy.mock.calls[0][1];
afterEach(() => {
vi.clearAllMocks();
});
it("with default arguments", async () => {
await utils.exportToSvg({

View file

@ -17,10 +17,11 @@ import {
} from "./test-utils";
import { defaultLang } from "../i18n";
import { FONT_FAMILY } from "../constants";
import { vi } from "vitest";
const { h } = window;
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
const mouse = new Pointer("mouse");
const finger1 = new Pointer("touch", 1);
@ -156,6 +157,7 @@ describe("regression tests", () => {
}
it("change the properties of a shape", () => {
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(10, 10);
togglePopover("Background");

View file

@ -9,11 +9,12 @@ import { ExcalidrawTextElement } from "../element/types";
import ExcalidrawApp from "../excalidraw-app";
import { API } from "./helpers/api";
import { KEYS } from "../keys";
import { vi } from "vitest";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
beforeEach(() => {
localStorage.clear();
renderScene.mockClear();

File diff suppressed because one or more lines are too long

View file

@ -61,7 +61,7 @@ describe("exportToSvg", () => {
);
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
`"themeFilter"`,
'"_themeFilter_f32792"',
);
});

View file

@ -13,11 +13,12 @@ import { reseed } from "../random";
import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import { SHAPES } from "../shapes";
import { vi } from "vitest";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const renderScene = jest.spyOn(Renderer, "renderScene");
const renderScene = vi.spyOn(Renderer, "renderScene");
beforeEach(() => {
localStorage.clear();
renderScene.mockClear();

View file

@ -160,7 +160,7 @@ export const throttleRAF = <T extends any[]>(
};
const ret = (...args: T) => {
if (process.env.NODE_ENV === "test") {
if (import.meta.env.MODE === "test") {
fn(...args);
return;
}
@ -772,7 +772,7 @@ export const arrayToMapWithIndex = <T extends { id: string }>(
return acc;
}, new Map<string, [element: T, index: number]>());
export const isTestEnv = () => process.env.NODE_ENV === "test";
export const isTestEnv = () => import.meta.env.MODE === "test";
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
return new CustomEvent(name, {

52
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,52 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/react" />
/// <reference types="vite-plugin-pwa/info" />
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
VITE_APP_BACKEND_V2_GET_URL: string;
VITE_APP_BACKEND_V2_POST_URL: string;
VITE_APP_LIBRARY_URL: string;
VITE_APP_LIBRARY_BACKEND: string;
// collaboration WebSocket server (https: string
VITE_APP_WS_SERVER_URL: string;
// set this only if using the collaboration workflow we use on excalidraw.com
VITE_APP_PORTAL_URL: string;
VITE_APP_FIREBASE_CONFIG: string;
// whether to enable Service Workers in development
VITE_APP_DEV_ENABLE_SW: string;
// whether to disable live reload / HMR. Usuaully what you want to do when
// debugging Service Workers.
VITE_APP_DEV_DISABLE_LIVE_RELOAD: string;
FAST_REFRESH: string;
// MATOMO
VITE_APP_MATOMO_URL: string;
VITE_APP_CDN_MATOMO_TRACKER_URL: string;
VITE_APP_MATOMO_SITE_ID: string;
//Debug flags
// To enable bounding box for text containers
VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX: string;
VITE_APP_DISABLE_SENTRY: string;
VITE_PKG_NAME: string;
VITE_PKG_VERSION: string;
VITE_IS_EXCALIDRAW_NPM_PACKAGE: string;
VITE_WORKER_ID: string;
MODE: string;
DEV: string;
PROD: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}