fix(import): use the open Project bin as destination
Resolve the shown Project bin for file-dialog and dropped imports. Capture the browser destination before asynchronous media reads so navigation cannot redirect an in-flight batch.
This commit is contained in:
parent
61238bf8fc
commit
aa66dd9c8d
|
|
@ -81,12 +81,12 @@ async fn prewarm(path: &str, size: u64) {
|
|||
}
|
||||
|
||||
/// `file.import` one path, retried while its bytes are loading.
|
||||
async fn import_path(path: String) -> Result<Value, String> {
|
||||
async fn import_path(path: String, bin: u64) -> Result<Value, String> {
|
||||
for _ in 0..2000 {
|
||||
let p = path.clone();
|
||||
let (r, pending) = on_ui(move |w, _| {
|
||||
let _ = filmcraft_media::pending::take();
|
||||
let r = w.app.session.execute("file.import", json!({"paths": [p]})).map_err(|e| e.to_string());
|
||||
let r = w.app.session.execute("file.import", json!({"paths": [p], "bin": bin})).map_err(|e| e.to_string());
|
||||
(r, filmcraft_media::pending::take())
|
||||
})
|
||||
.await;
|
||||
|
|
@ -105,6 +105,8 @@ async fn import_path(path: String) -> Result<Value, String> {
|
|||
|
||||
/// Import browser files (no copies): returns `{items, errors, paths}`.
|
||||
pub async fn import_files(files: Vec<web_sys::File>) -> Value {
|
||||
// Capture once per batch: navigation while prewarming must not redirect pending imports.
|
||||
let bin = on_ui(|w, _| w.app.import_bin().0).await;
|
||||
let mut items = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
let mut paths = Vec::new();
|
||||
|
|
@ -113,7 +115,7 @@ pub async fn import_files(files: Vec<web_sys::File>) -> Value {
|
|||
let size = f.size() as u64;
|
||||
let path = fs::register_blob(&name, f.clone().into());
|
||||
prewarm(&path, size).await;
|
||||
match import_path(path.clone()).await {
|
||||
match import_path(path.clone(), bin).await {
|
||||
Ok(v) => {
|
||||
items.extend(v.get("items").and_then(Value::as_array).cloned().unwrap_or_default());
|
||||
errors.extend(v.get("errors").and_then(Value::as_array).cloned().unwrap_or_default());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
// Web import destinations: active bin tab, browser drop + navigation during pending reads.
|
||||
// Synthetic browser drop event exercises the production handler; physical OS drag/picker UI is outside this test.
|
||||
export async function checkImportBins({ evaluate, media, timeoutMs = 120000 }) {
|
||||
const report = {gates: {}, cases: []};
|
||||
await evaluate('filmcraft.execute("file.newProject", {name: "Import bins"})');
|
||||
const parent = (await evaluate('filmcraft.execute("file.newBin", {name: "Footage"})')).bin;
|
||||
const child = (await evaluate(`filmcraft.execute("file.newBin", {name: "Selects", parent: ${parent}})`)).bin;
|
||||
await evaluate(`(async () => {window.__importBinFixture = new Uint8Array(await (await fetch(${JSON.stringify(media)})).arrayBuffer());})()`);
|
||||
const setBin = bin => evaluate(`filmcraft.request("ui.set", {projectPanel: {bin: ${JSON.stringify(bin)}, activeTab: null}})`);
|
||||
const importExpression = name => `filmcraft.importFiles([new File([window.__importBinFixture], ${JSON.stringify(name)}, {type: "video/mp4"})])`;
|
||||
const locate = async name => {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const pollMs = 100;
|
||||
for (;;) {
|
||||
const project = await evaluate('filmcraft.execute("project.inspect")');
|
||||
const find = bin => {
|
||||
for (const entry of bin.children) {
|
||||
if (entry.item !== undefined && entry.name === name) return bin.id;
|
||||
if (entry.children) {const found = find(entry); if (found !== undefined) return found;}
|
||||
}
|
||||
};
|
||||
const found = find(project.root);
|
||||
if (found !== undefined) return found;
|
||||
if (Date.now() >= deadline) throw new Error(`Import never appeared: ${name}`);
|
||||
await new Promise(resolve => setTimeout(resolve, pollMs));
|
||||
}
|
||||
};
|
||||
const check = async (key, expected, name) => {
|
||||
const actual = await locate(name);
|
||||
report.cases.push({key, expected, actual, name}); report.gates[key] = actual === expected;
|
||||
};
|
||||
await evaluate(`filmcraft.request("ui.set", {projectPanel: {bin: ${child}, activeTab: 0, tabs: [{bin: ${parent}}]}})`);
|
||||
await evaluate(importExpression("tab.mp4")); await check("activeTab", parent, "tab.mp4");
|
||||
await setBin(child);
|
||||
await evaluate(`(() => {
|
||||
const transfer = new DataTransfer();
|
||||
transfer.items.add(new File([window.__importBinFixture], "drop.mp4", {type: "video/mp4"}));
|
||||
window.dispatchEvent(new DragEvent("drop", {dataTransfer: transfer, bubbles: true, cancelable: true}));
|
||||
})()`);
|
||||
await check("browserDrop", child, "drop.mp4");
|
||||
await setBin(child);
|
||||
await evaluate(`(() => {
|
||||
const original = Blob.prototype.arrayBuffer;
|
||||
let release;
|
||||
const pending = new Promise(resolve => {release = resolve;});
|
||||
const gate = window.__importBinGate = {hits: 0, release, original};
|
||||
Blob.prototype.arrayBuffer = function () {gate.hits++; return pending.then(() => original.call(this));};
|
||||
window.__pendingBinImport = ${importExpression("pending.mp4")};
|
||||
})()`);
|
||||
try {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const pollMs = 100;
|
||||
for (;;) {
|
||||
if (await evaluate("window.__importBinGate.hits > 0")) break;
|
||||
if (Date.now() >= deadline) throw new Error("Read-delay probe did not intercept any Blob reads");
|
||||
await new Promise(resolve => setTimeout(resolve, pollMs));
|
||||
}
|
||||
report.delayedReadHits = await evaluate("window.__importBinGate.hits");
|
||||
await setBin(parent);
|
||||
await evaluate("window.__importBinGate.release(); window.__pendingBinImport");
|
||||
await check("destinationCapturedBeforeRead", child, "pending.mp4");
|
||||
} finally {
|
||||
await evaluate("Blob.prototype.arrayBuffer = window.__importBinGate.original; window.__importBinGate.release(); delete window.__importBinGate; delete window.__pendingBinImport; delete window.__importBinFixture");
|
||||
}
|
||||
report.ok = Object.values(report.gates).every(Boolean);
|
||||
return report;
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import { spawn } from "node:child_process";
|
|||
import { mkdirSync, writeFileSync, readdirSync, statSync, mkdtempSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { checkImportBins } from "./import-bins.mjs";
|
||||
|
||||
const arg = (k, d) => {
|
||||
const i = process.argv.indexOf(`--${k}`);
|
||||
|
|
@ -162,6 +163,8 @@ try {
|
|||
await sleep(500);
|
||||
await send("Emulation.setDeviceMetricsOverride", { width: 1600, height: 1000, deviceScaleFactor: 1, mobile: false });
|
||||
report.steps.modes = { screenshot: await shot("05-modes") };
|
||||
report.steps.importBins = await checkImportBins({evaluate: js, media});
|
||||
if (!report.steps.importBins.ok) throw new Error("import bin regression: " + JSON.stringify(report.steps.importBins.gates));
|
||||
// still alive? (a panicked app never answers)
|
||||
await Promise.race([js("filmcraft.inspect().then(() => true)"), sleep(5000).then(() => { throw new Error("app stopped answering"); })]);
|
||||
const fatal = logs.filter((l) => /panicked at|RuntimeError|EXCEPTION/.test(l));
|
||||
|
|
|
|||
|
|
@ -938,6 +938,11 @@ impl FilmcraftApp {
|
|||
|
||||
// ---------------------------------------------------------------- files
|
||||
|
||||
/// Destination of imports started from the currently shown Project panel view.
|
||||
pub fn import_bin(&self) -> filmcraft_project::BinId {
|
||||
panels::project::view_of(self, panels::project::shown_inst(self)).bin
|
||||
}
|
||||
|
||||
pub fn file_dialog(&mut self, id: &str, params: &Value) -> Result<Value, String> {
|
||||
match id {
|
||||
"file.import" => {
|
||||
|
|
@ -952,7 +957,8 @@ impl FilmcraftApp {
|
|||
if paths.is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let r = self.session.execute("file.import", json!({"paths": paths})).map_err(|e| e.to_string());
|
||||
let bin = params.get("bin").cloned().unwrap_or_else(|| json!(self.import_bin().0));
|
||||
let r = self.session.execute("file.import", json!({"paths": paths, "bin": bin})).map_err(|e| e.to_string());
|
||||
if let Ok(v) = &r
|
||||
&& let Some(errs) = v.get("errors").and_then(Value::as_array)
|
||||
&& !errs.is_empty()
|
||||
|
|
@ -1022,7 +1028,7 @@ impl FilmcraftApp {
|
|||
}
|
||||
}
|
||||
if !paths.is_empty() {
|
||||
let _ = self.session.execute("file.import", json!({"paths": paths}));
|
||||
let _ = self.session.execute("file.import", json!({"paths": paths, "bin": self.import_bin().0}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -492,6 +492,34 @@ fn media_dir(tag: &str) -> std::path::PathBuf {
|
|||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_dialog_import_uses_shown_bin_unless_explicit() {
|
||||
let dir = media_dir("import-bin");
|
||||
let path = dir.join("a-interview.wav").to_string_lossy().into_owned();
|
||||
let mut app = FilmcraftApp::new(Session::default());
|
||||
app.hooks.pick_files = Some(Box::new(move |_| vec![path.clone()]));
|
||||
let parent = app.session.execute("file.newBin", json!({"name": "Footage"})).unwrap()["bin"].as_u64().unwrap();
|
||||
let child = app.session.execute("file.newBin", json!({"name": "Selects", "parent": parent})).unwrap()["bin"].as_u64().unwrap();
|
||||
let root = app.session.project.root.id.0;
|
||||
app.ui.project_panel.tabs.push(filmcraft_ui_egui::panels::project::BinTab { bin: parent, ..Default::default() });
|
||||
for (shown, tab, params, expected) in [
|
||||
(None, None, json!({}), root),
|
||||
(Some(child), None, json!({}), child),
|
||||
(Some(child), Some(0), json!({}), parent),
|
||||
(Some(child), None, json!({"bin": null}), root),
|
||||
(Some(parent), None, json!({"bin": child}), child),
|
||||
] {
|
||||
app.ui.project_panel.bin = shown;
|
||||
app.ui.project_panel.active_tab = tab;
|
||||
let result = app.file_dialog("file.import", ¶ms).unwrap();
|
||||
assert!(result["errors"].as_array().unwrap().is_empty(), "{result}");
|
||||
let item = ItemId(result["items"][0].as_u64().unwrap());
|
||||
let bin = app.session.project.root.find_bin(filmcraft_project::BinId(expected)).unwrap();
|
||||
assert!(bin.children.contains(&BinEntry::Item(item)), "shown={shown:?}, tab={tab:?}, params={params}: expected bin {expected}");
|
||||
}
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_browser_navigation_import_and_columns() {
|
||||
let dir = media_dir("nav");
|
||||
|
|
|
|||
Loading…
Reference in New Issue