diff --git a/frontend/test/frontend_tests/composable_tests/generated_test.cljs b/frontend/test/frontend_tests/composable_tests/generated_test.cljs index 42bfb358b2..13af186a2c 100644 --- a/frontend/test/frontend_tests/composable_tests/generated_test.cljs +++ b/frontend/test/frontend_tests/composable_tests/generated_test.cljs @@ -51,12 +51,14 @@ pure (:situation state) live (ftm/refresh-generated live) expected (cg/resolve-op pure (:model state) tuple) - actual (if (#{:undo :redo} (first tuple)) + actual (if (#{:undo :redo :library-undo :library-redo} (first tuple)) expected (cg/resolve-op live (live-model state live) tuple))] (t/is (= (:trace expected) (:trace actual)) (str "Replay changed a target: " tuple)) (let [next-live (try (if-let [op (:op actual)] - (await (ftm/run-generated-step live op)) + (if-let [owner (:library-id actual)] + (await (ftm/run-generated-source-step live owner op)) + (await (ftm/run-generated-step live op))) (do (t/is (= :settled (await (ftm/await-step [])))) (ftm/refresh-generated live))) @@ -82,7 +84,7 @@ (oracle/check next-live (live-model next-state next-live)) (catch :default error (throw (ex-info (str "Frontend oracle at " tuple ": " (ex-message error)) - (ex-data error) error)))) + (assoc (ex-data error) :replay-tuple tuple) error)))) (let [parity {:relative-free-head-placement true} expected (oracle/canonical (:situation next-state) parity) actual (oracle/canonical next-live parity)] @@ -107,7 +109,7 @@ (t/is false (str (ex-message error) "\nReplay: " (pr-str program) "\nDetails: " (pr-str (ex-data error))))))))) -(defn- ^:async replay-known-source-routing-failure +(defn- ^:async replay-known-library-reference-failure [program] (await (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock} @@ -130,10 +132,9 @@ (ftm/uninstall!)) (let [actual (try (await (replay-program program)) :success (catch :default error - (select-keys (ex-data error) [:type :code :hint :replay-tuple])))] - (tm/known-failure :F248 :success - {:type :assertion :code :data-validation :hint "expected valid shape" - :replay-tuple [:library-add-child 0]} + (select-keys (ex-data error) [:type :code :hint :invariant :phase :group :replay-tuple])))] + (tm/known-failure :F249 :success + {:invariant :I4 :group :structure :replay-tuple [:library-sync]} actual))))) (def update-main-intent-programs @@ -216,9 +217,57 @@ :ops [[:grid-cell 0 0 0 2] [:library-add-child 0] [:library-add-child 0] [:edit 0 1 1 2] [:library-sync] [:undo] [:redo]]}])] (if (get-in program [:world 2 :stale-libraries]) - (await (replay-known-source-routing-failure program)) + (await (replay-known-library-reference-failure program)) (await (replay program))))) +(t/deftest ^:async source-grid-additions-replay-without-accepting-the-library + (await (replay {:world [1 0 {:library true :library-depth 0 :library-grid true :stale-libraries true}] + :ops [[:grid-cell 0 0 0 2] [:library-add-child 0] [:library-add-child 0] + [:edit 0 1 1 2] [:undo] [:redo]]}))) + +(t/deftest ^:async source-editors-retain-their-own-undo-and-redo-histories + (doseq [ops [[[:library-add-child 0] [:library-add-child 0] [:library-undo 0] + [:edit 0 1 1 2] [:library-redo 0] [:undo] [:redo]] + [[:edit 0 1 1 2] [:library-edit 0 0 1] [:undo] + [:library-undo 0] [:redo] [:library-redo 0]] + [[:library-transform 0 0 1 0] [:edit 0 1 1 2] + [:library-undo 0] [:undo] [:library-redo 0] [:redo]]]] + (await (replay {:world [1 0 {:library true :library-depth 0 :library-grid true :stale-libraries true}] + :ops ops})))) + +(t/deftest ^:async two-source-editors-keep-their-histories-separate + (await (replay {:world [1 0 {:library true :library-depth 0 :library-dependency true :stale-libraries true}] + :ops [[:library-edit 0 0 1] [:library-edit 1 0 2] + [:library-undo 0] [:library-undo 1] + [:library-redo 0] [:library-redo 1]]}))) + +(t/deftest ^:async source-grid-acceptance-preserves-authored-cells-and-undo + (await + (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock rx/timer mock/timer-mock} + (let [initial (cg/world [1 0 {:library true :library-depth 0 :library-grid true :stale-libraries true}]) + edit (cg/resolve-op initial {} [:grid-cell 0 0 0 2]) + id (get-in edit [:shape :id]) + props (get-in edit [:op :value :props]) + live (ftm/start-generated initial) + live (await (ftm/run-generated-step live (:op edit))) + live (loop [live live tuples [[:library-add-child 0] [:library-add-child 0] [:edit 0 1 1 2]]] + (if-let [tuple (first tuples)] + (let [{:keys [op library-id]} (cg/resolve-op live {} tuple) + after (if library-id + (await (ftm/run-generated-source-step live library-id op)) + (await (ftm/run-generated-step live op)))] + (recur after (rest tuples))) + live)) + live (await (ftm/run-generated-step live (:op (cg/resolve-op live {} [:library-sync])))) + shape (tm/shape-by-id live id) + cell (first (filter #(= [1 1] [(:row %) (:column %)]) (vals (:layout-grid-cells shape))))] + (t/is (some? (cthf/validate-file! (tm/file live) (cf/libraries live)))) + (t/is (= 5 (count (:shapes shape)))) + (t/is (= [2 3] (mapv #(count (get shape %)) [:layout-grid-rows :layout-grid-columns]))) + (t/is (= props (select-keys cell (keys props)))) + (t/is (empty? (uc/failures (await (ftm/finish-generated live))))) + (ftm/uninstall!))))) + (t/deftest ^:async authored-grid-histories-use-independent-cell-expectations (doseq [ops [[[:grid-direction 0 0 1 1] [:grid-direction 0 0 1 0] [:grid-direction 0 0 0 0] [:update-main 0] diff --git a/frontend/test/frontend_tests/composable_tests/interpreter.cljs b/frontend/test/frontend_tests/composable_tests/interpreter.cljs index 0a6418080f..5686830185 100644 --- a/frontend/test/frontend_tests/composable_tests/interpreter.cljs +++ b/frontend/test/frontend_tests/composable_tests/interpreter.cljs @@ -872,6 +872,63 @@ [situation op] (run-ops situation [op])) +(defn- get-generated-editor + [situation] + {:situation (dissoc (refresh-generated situation) ::source-editors) + :undo (:workspace-undo @st/state)}) + +(defn- resume-generated-editor + [{:keys [situation undo]}] + (let [file (tm/file situation)] + (uninstall!) + (st/emit! (ptk/reify ::resume-generated-editor + ptk/UpdateEvent + (update [_ state] + (assoc state :current-file-id (:id file) + :current-page-id (cthf/current-page-id file) + :workspace-undo undo))) + (dw/initialize-edit-watchers) + (dwl/watch-library-changes) + (headless-text-measurement)) + (let [situation (refresh-generated situation) + baseline (uc/baseline situation) + current (snapshot)] + (if baseline + ;; Another editor can change loaded libraries between this editor's + ;; steps. Keep its own undo baseline and refresh only foreign files. + (uc/mark-baseline situation (:depth baseline) + (assoc current :files + (assoc (:files current) (:id file) + (get-in baseline [:snapshot :files (:id file)])))) + (mark-baseline situation))))) + +(defn ^:async run-generated-source-step + "Run a source edit in its owning editor and retain both undo histories. + Returning to the consumer loads source data without accepting it." + [situation owner op] + (let [consumer (get-generated-editor situation) + files (cf/all-files situation) + editor (or (get-in situation [::source-editors owner]) + {:situation (-> (tm/make-situation (get files owner)) + (tm/with-aux-files (dissoc files owner)) + (assoc ::undo-check? true ::generated true) + (uc/with-runner :frontend)) + :undo {}}) + source (resume-generated-editor editor) + before (snapshot) + source (await (run-ops source [op]))] + (when-not source + (throw (ex-info "Source editor step failed" {:owner owner}))) + (t/is (= (dissoc (:files before) owner) (dissoc (:files (snapshot)) owner)) + "A source editor must not write another file") + (let [source (await (check-variant source)) + editor (get-generated-editor source) + live (resume-generated-editor consumer)] + (t/is (empty? (uc/failures source)) (pr-str (uc/failures source))) + (-> live + (assoc ::source-editors (assoc (::source-editors situation) owner editor)) + (tm/record-application op {:library-id owner}))))) + (defn finish-generated [situation] (check-variant situation))