mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-12 00:42:53 +00:00
Keep published links and fix onboarding review findings
This commit is contained in:
1 parent
b2a7a3c029
commit
f148ddcf34
10 files changed
+147
-11
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { BrowserWindow } from "electron";
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
|
||||
class TestWindow extends EventEmitter {
|
||||
webContents = Object.assign(new EventEmitter(), {
|
||||
ipc: new EventEmitter(),
|
||||
mainFrame: {},
|
||||
});
|
||||
private destroyed = false;
|
||||
private visible = false;
|
||||
setIgnoreMouseEvents = vi.fn();
|
||||
setBounds = vi.fn();
|
||||
setOpacity = vi.fn();
|
||||
moveTop = vi.fn();
|
||||
showInactive() {
|
||||
this.visible = true;
|
||||
}
|
||||
isVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
isDestroyed() {
|
||||
return this.destroyed;
|
||||
}
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
this.emit("closed");
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("electron", () => ({ BrowserWindow: TestWindow }));
|
||||
|
||||
const { startOnboardingIntro } = await import("./onboardingIntro");
|
||||
|
||||
beforeEach(() =>
|
||||
vi.useFakeTimers({
|
||||
toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "performance"],
|
||||
}),
|
||||
);
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
function start() {
|
||||
const popup = new TestWindow();
|
||||
const show = vi.fn();
|
||||
const backdrop = startOnboardingIntro(
|
||||
{ x: 0, y: 0, width: 1440, height: 900 },
|
||||
"/test/preload.js",
|
||||
vi.fn(),
|
||||
{ popup: popup as unknown as BrowserWindow, show },
|
||||
) as unknown as TestWindow;
|
||||
const stage = (name: string) =>
|
||||
backdrop.webContents.ipc.emit(
|
||||
"onboarding-intro",
|
||||
{ senderFrame: backdrop.webContents.mainFrame },
|
||||
name,
|
||||
);
|
||||
return { popup, show, backdrop, stage };
|
||||
}
|
||||
|
||||
it("waits for both the intro and popup, then fades the backdrop out", () => {
|
||||
const { popup, show, backdrop, stage } = start();
|
||||
stage("ready");
|
||||
stage("finished");
|
||||
expect(show).not.toHaveBeenCalled();
|
||||
popup.emit("ready-to-show");
|
||||
expect(show).toHaveBeenCalledOnce();
|
||||
expect(popup.setOpacity).toHaveBeenCalledWith(0);
|
||||
vi.advanceTimersByTime(660);
|
||||
expect(popup.setOpacity).toHaveBeenLastCalledWith(1);
|
||||
expect(backdrop.isDestroyed()).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["skip", "timeout"])("shows a ready popup after intro %s", (mode) => {
|
||||
const { popup, show, backdrop, stage } = start();
|
||||
popup.emit("ready-to-show");
|
||||
expect(show).not.toHaveBeenCalled();
|
||||
if (mode === "skip") stage("skipped");
|
||||
else vi.advanceTimersByTime(6000);
|
||||
expect(show).toHaveBeenCalledOnce();
|
||||
expect(backdrop.isDestroyed()).toBe(true);
|
||||
expect(popup.setOpacity).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -93,3 +93,25 @@ it("keeps a known nonempty display snapshot during an expired background reload"
|
||||
await pending;
|
||||
expect(cache.peekRecentSharedVideos("account")).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses the newer result when a forced refresh overtakes an in-flight list", async () => {
|
||||
const old = { code: "old", title: "Old", url: "https://share.test/s/old", ready: true };
|
||||
let release!: (result: { success: boolean; videos: (typeof old)[] }) => void;
|
||||
list.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
release = resolve;
|
||||
}),
|
||||
).mockResolvedValueOnce({ success: true, videos: [] });
|
||||
const read = (fresh = false) =>
|
||||
cache.readRecentSharedVideos("account", "https://share.test", "token", fresh);
|
||||
const earlier = read();
|
||||
await Promise.resolve();
|
||||
const refreshed = read(true);
|
||||
expect(read()).toBe(refreshed);
|
||||
await expect(refreshed).resolves.toEqual([]);
|
||||
release({ success: true, videos: [old] });
|
||||
await earlier;
|
||||
expect(cache.peekRecentSharedVideos("account")).toEqual([]);
|
||||
expect(list).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -27,7 +27,7 @@ export function readRecentSharedVideos(
|
||||
fresh = false,
|
||||
): Promise<SharedVideo[]> {
|
||||
const existing = snapshots.get(key);
|
||||
if (existing?.pending) return existing.pending;
|
||||
if (existing?.pending && !fresh) return existing.pending;
|
||||
if (!fresh && existing?.videos && existing.expiresAt > Date.now())
|
||||
return Promise.resolve(existing.videos);
|
||||
const snapshot: Snapshot = { videos: existing?.videos, expiresAt: existing?.expiresAt ?? 0 };
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PublishLinkIcon, PublishSaveIcon } from "./PublishDestinationIcons";
|
||||
type Props = {
|
||||
projectRevision?: string;
|
||||
projectPath?: string | null;
|
||||
projectSourcePath?: string | null;
|
||||
t: ReturnType<typeof useI18n>["t"];
|
||||
videoPlaybackRef?: RefObject<VideoPlaybackRef | null>;
|
||||
exportSettings: ReturnType<typeof useExportSettings>;
|
||||
@@ -52,6 +53,7 @@ export function EditorExportMenu(props: Props) {
|
||||
const { publishDestination: destination, setPublishDestination: setDestination } =
|
||||
props.exportSettings;
|
||||
const [shareBusy, setShareBusy] = useState(false);
|
||||
const [hasStartedShare, setHasStartedShare] = useState(false);
|
||||
const [shareActivity, setShareActivity] = useState(false);
|
||||
const [shareContainer, setShareContainer] = useState<HTMLDivElement | null>(null);
|
||||
const [thumbnail, setThumbnail] = useState<string>();
|
||||
@@ -90,6 +92,7 @@ export function EditorExportMenu(props: Props) {
|
||||
(busy: boolean) => {
|
||||
setShareBusy(busy);
|
||||
if (busy) {
|
||||
setHasStartedShare(true);
|
||||
captureThumbnail();
|
||||
setShareActivity(true);
|
||||
setShowExportDropdown(false);
|
||||
@@ -333,8 +336,9 @@ export function EditorExportMenu(props: Props) {
|
||||
</motion.div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{destination === "link" && (publishMenuOpen || shareActivity || shareBusy) && (
|
||||
{(hasStartedShare || (destination === "link" && publishMenuOpen)) && (
|
||||
<CloudShareButton
|
||||
key={props.projectPath ?? props.projectSourcePath ?? undefined}
|
||||
inline
|
||||
projectPath={props.projectPath}
|
||||
projectRevision={props.projectRevision}
|
||||
@@ -350,8 +354,9 @@ export function EditorExportMenu(props: Props) {
|
||||
props.onRequestShareSignIn();
|
||||
}}
|
||||
floatingActivity={{
|
||||
formContainer: publishMenuOpen ? shareContainer : null,
|
||||
visible: shareActivity,
|
||||
formContainer:
|
||||
publishMenuOpen && destination === "link" ? shareContainer : null,
|
||||
visible: destination === "link" && shareActivity,
|
||||
thumbnail,
|
||||
onDismiss: dismissShareActivity,
|
||||
onRetry: () => {
|
||||
|
||||
@@ -278,6 +278,7 @@ export function EditorHeader(props: Props) {
|
||||
handleStartExportFromDropdown={handleStartExportFromDropdown}
|
||||
revealExportedFile={revealExportedFile}
|
||||
projectPath={project.currentProjectPath}
|
||||
projectSourcePath={project.videoSourcePath}
|
||||
projectRevision={props.projectRevision}
|
||||
projectTitle={projectDisplayName}
|
||||
prepareExportForShare={props.prepareExportForShare}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function useProjectLifecycle(input: Input) {
|
||||
appearance.setBorderRadius(editor.borderRadius);
|
||||
appearance.setPadding(editor.padding);
|
||||
appearance.setCropRegion(editor.cropRegion);
|
||||
appearance.setWebcam({
|
||||
const webcam = {
|
||||
...editor.webcam,
|
||||
loop:
|
||||
editor.webcam.loop ??
|
||||
@@ -204,7 +204,8 @@ export function useProjectLifecycle(input: Input) {
|
||||
editor.webcam.sourcePath ? fromFileUrl(editor.webcam.sourcePath) : null,
|
||||
editor.webcam.timeOffsetMs,
|
||||
)),
|
||||
});
|
||||
};
|
||||
appearance.setWebcam(webcam);
|
||||
timeline.setZoomRegions(editor.zoomRegions);
|
||||
timeline.setTrimRegions(editor.trimRegions);
|
||||
timeline.setClipRegions(editor.clipRegions);
|
||||
@@ -245,13 +246,14 @@ export function useProjectLifecycle(input: Input) {
|
||||
refs.nextAnnotationZIndexRef.current =
|
||||
editor.annotationRegions.reduce((max, region) => Math.max(max, region.zIndex), 0) +
|
||||
1;
|
||||
const restoredHistory = restoreEditorHistory(loadedProject.history, editor);
|
||||
const appliedEditor = { ...editor, webcam };
|
||||
const restoredHistory = restoreEditorHistory(loadedProject.history, appliedEditor);
|
||||
current.restoreHistory(restoredHistory);
|
||||
project.setLastSavedSnapshot(
|
||||
cloneStructured(
|
||||
createProjectData(
|
||||
sourcePath,
|
||||
current.buildPersistedEditorState(editor),
|
||||
current.buildPersistedEditorState(appliedEditor),
|
||||
loadedProject.projectId ?? null,
|
||||
persistEditorHistory(restoredHistory),
|
||||
),
|
||||
|
||||
@@ -72,6 +72,10 @@ describe("getWebcamMediaTargetTimeSeconds", () => {
|
||||
it("repeats older uploads while preserving timing for paired webcam recordings", () => {
|
||||
expect(shouldLoopWebcamSource("/clips/demo.mp4", "/downloads/camera.mp4")).toBe(true);
|
||||
expect(shouldLoopWebcamSource("/clips/demo.mp4", "/clips/demo-webcam.webm")).toBe(false);
|
||||
expect(shouldLoopWebcamSource("/clips/demo.mp4", "/relocated/demo-webcam.webm")).toBe(false);
|
||||
expect(shouldLoopWebcamSource("C:\\clips\\demo.mp4", "D:\\relocated\\demo-webcam.webm")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(shouldLoopWebcamSource("/clips/demo.mp4", "/camera.mp4", 150)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@ export function shouldLoopWebcamSource(
|
||||
timeOffsetMs = 0,
|
||||
): boolean {
|
||||
if (!videoPath || !webcamPath || timeOffsetMs !== 0) return false;
|
||||
const stem = (path: string) => path.replace(/\\/g, "/").replace(/\.[^./]+$/, "");
|
||||
const stem = (path: string) =>
|
||||
path
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.replace(/\.[^.]+$/, "");
|
||||
const videoStem = stem(videoPath);
|
||||
const webcamStem = stem(webcamPath);
|
||||
return webcamStem !== videoStem && webcamStem !== `${videoStem}-webcam`;
|
||||
|
||||
+2
-1
@@ -262,7 +262,8 @@
|
||||
@media (forced-colors: active) {
|
||||
:is(.input, .textarea):focus,
|
||||
.input-group:has(.input-group__input:focus),
|
||||
.share-link-field:has(> input:focus) {
|
||||
.share-link-field:has(> input:focus),
|
||||
[data-error-report] > button:focus-visible {
|
||||
outline: 2px solid Highlight;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ for (const full of [false, true]) {
|
||||
test("publish preserves local destination and sharing continues in the background", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
await page.goto("/tests/ui/publish.html?switch");
|
||||
await page.getByRole("button", { name: "Publish", exact: true }).click();
|
||||
await page.getByRole("row", { name: "Save to device", exact: true }).click();
|
||||
@@ -68,10 +69,24 @@ test("publish preserves local destination and sharing continues in the backgroun
|
||||
.getByRole("button", { name: "Fixture refresh token", exact: true })
|
||||
.evaluate((button: HTMLButtonElement) => button.click());
|
||||
await expect(page.getByRole("button", { name: "Copy link", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("region", { name: "Create link" })).toHaveCount(0, {
|
||||
timeout: 15_000,
|
||||
});
|
||||
await page.getByRole("button", { name: "Publish", exact: true }).click();
|
||||
await expect(page.getByRole("textbox", { name: "Share link" })).toHaveValue(
|
||||
"https://example.test/s/preview",
|
||||
);
|
||||
await page.getByRole("row", { name: "Save to device", exact: true }).click();
|
||||
await page.getByRole("row", { name: "Create share link", exact: true }).click();
|
||||
await expect(page.getByRole("textbox", { name: "Share link" })).toHaveValue(
|
||||
"https://example.test/s/preview",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
await page
|
||||
.getByRole("button", { name: "Fixture switch account", exact: true })
|
||||
.evaluate((button: HTMLButtonElement) => button.click());
|
||||
await expect(page.getByRole("button", { name: "Copy link", exact: true })).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Publish", exact: true }).click();
|
||||
await expect(page.getByRole("textbox", { name: "Share link" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a full cloud quota prevents starting preparation", async ({ page }) => {
|
||||
|
||||
Reference in new issue
Block a user