Skip to content

Commit 180da8a

Browse files
committed
Cover the compare and merge view in the chat input, skill files, prompt editor and terminal files
Browser tests for the side-by-side compare view: opening it from the chat input's More menu and the /compare command, Use in prompt filling the message without sending it, stepping through changes, copying a change to either side, identical texts showing no differences and Cancel asking before it discards. A revision applied in the skill file editor and the prompt editor is what gets saved, the skill file's Copy copies the open file, and two terminal files picked for comparison open side by side and swap.
1 parent 817adc3 commit 180da8a

5 files changed

Lines changed: 289 additions & 9 deletions

File tree

‎e2e/chat/test_compare_text.py‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Journey: comparing a draft with a revision in the chat input's Compare text view.
2+
3+
More > Compare text opens the side-by-side compare and merge view with the message being typed on
4+
the left as the original. Use in prompt puts the revision on the right into the message box and
5+
sends nothing until the person sends it. In the comparison the view counts the changes ("Change 1
6+
of 2") and steps through them with Previous change and Next change, copies the current change from
7+
the original to the proposed revision or back, and says "No differences" for two identical texts.
8+
Closing after an edit asks to discard the comparison first and leaves the message as it was;
9+
closing with nothing edited asks nothing. Opening the view from the `/compare` command is
10+
e2e/chat/test_slash_commands.py.
11+
12+
Discriminates: passes on the dev 3b8205505 build (backend at 3b8205505, frontend of 226dca18c,
13+
which differs only in the chat input's selected-feature pills). In a build of it whose Use in prompt
14+
also sends the message the Use in prompt test fails, in one whose copy buttons copy the other way
15+
the copy test fails, in one whose Next change stays put the step-through test fails, in one that
16+
counts identical texts as one change the no-differences test fails, and in one whose Cancel closes
17+
without asking the discard test fails.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import re
23+
import uuid
24+
25+
import pytest
26+
from playwright.sync_api import Locator, Page, expect
27+
28+
from harness import upstream as reply
29+
from utils.chat_ui import chat_input, expect_reply, open_input_menu
30+
31+
pytestmark = [pytest.mark.journey, pytest.mark.requires_browser, pytest.mark.requires_source]
32+
33+
ORIGINAL = "alpha\nbravo\ncharlie\ndelta\necho\nfoxtrot\ngolf"
34+
REVISED = "alpha\nBRAVO\ncharlie\ndelta\necho\nFOXTROT\ngolf"
35+
36+
37+
def _type_draft(page: Page, text: str) -> None:
38+
expect(chat_input(page)).to_be_visible()
39+
chat_input(page).click()
40+
page.keyboard.type(text)
41+
expect(chat_input(page)).to_have_text(text)
42+
43+
44+
def _open_compare(page: Page) -> Locator:
45+
open_input_menu(page).get_by_role("menuitem", name="Compare text").click()
46+
compare = page.get_by_role("dialog", name="Compare text")
47+
expect(compare).to_be_visible()
48+
return compare
49+
50+
51+
def _pane(compare: Locator, label: str) -> Locator:
52+
return compare.get_by_role("textbox", name=label, exact=True)
53+
54+
55+
def _write(compare: Locator, label: str, text: str) -> None:
56+
_pane(compare, label).click()
57+
compare.page.keyboard.insert_text(text)
58+
59+
60+
def _start_comparing(compare: Locator, original: str, revised: str) -> Locator:
61+
"""Write both sides, press Compare and return the change counter."""
62+
_write(compare, "Original", original)
63+
_write(compare, "Proposed revision", revised)
64+
compare.get_by_role("button", name="Compare", exact=True).click()
65+
return compare.get_by_role("status")
66+
67+
68+
def test_use_in_prompt_puts_the_revision_in_the_input_without_sending(
69+
page_for, make_user, upstream
70+
):
71+
page = page_for(make_user())
72+
draft = f"Tell me about the tides {uuid.uuid4().hex[:6]}"
73+
revised = f"{draft}, in one short sentence"
74+
_type_draft(page, draft)
75+
sent_before = len(upstream.chat_requests())
76+
77+
compare = _open_compare(page)
78+
expect(_pane(compare, "Original")).to_have_text(draft)
79+
_pane(compare, "Proposed revision").click()
80+
page.keyboard.insert_text(revised)
81+
compare.get_by_role("button", name="Use in prompt").click()
82+
83+
expect(compare).to_have_count(0)
84+
expect(chat_input(page)).to_have_text(revised)
85+
# a bounded look that nothing is sent on its own after the revision lands
86+
page.wait_for_timeout(1000)
87+
assert len(upstream.chat_requests()) == sent_before, "Use in prompt sent the message itself"
88+
expect(page).not_to_have_url(re.compile(r"/c/"))
89+
90+
upstream.queue(reply.text("Noted.", match=reply.answering(revised)))
91+
chat_input(page).press("Enter")
92+
expect_reply(page, "Noted.")
93+
[request] = [body for body in upstream.chat_requests() if reply.answering(revised)(body)]
94+
assert request["messages"][-1]["content"] == revised
95+
96+
97+
def test_next_and_previous_change_step_through_the_changes(page_for, make_user):
98+
page = page_for(make_user())
99+
compare = _open_compare(page)
100+
counter = _start_comparing(compare, ORIGINAL, REVISED)
101+
previous = compare.get_by_role("button", name="Previous change")
102+
following = compare.get_by_role("button", name="Next change")
103+
104+
expect(counter).to_have_text("Change 1 of 2")
105+
expect(previous).to_be_disabled()
106+
following.click()
107+
expect(counter).to_have_text("Change 2 of 2")
108+
expect(following).to_be_disabled()
109+
previous.click()
110+
expect(counter).to_have_text("Change 1 of 2")
111+
112+
113+
def test_a_change_is_copied_to_the_proposal_or_back_to_the_original(page_for, make_user):
114+
page = page_for(make_user())
115+
compare = _open_compare(page)
116+
counter = _start_comparing(compare, ORIGINAL, REVISED)
117+
expect(counter).to_have_text("Change 1 of 2")
118+
119+
compare.get_by_role("button", name="Copy change to proposal").click()
120+
expect(counter).to_have_text("Change 1 of 1")
121+
expect(_pane(compare, "Proposed revision")).to_contain_text("bravo")
122+
expect(_pane(compare, "Proposed revision")).not_to_contain_text("BRAVO")
123+
124+
compare.get_by_role("button", name="Copy change to original").click()
125+
expect(counter).to_have_text("No differences")
126+
expect(_pane(compare, "Original")).to_contain_text("FOXTROT")
127+
expect(_pane(compare, "Original")).not_to_contain_text("foxtrot")
128+
129+
compare.get_by_role("button", name="Use in prompt").click()
130+
expect(compare).to_have_count(0)
131+
expect(chat_input(page)).to_contain_text("bravo")
132+
expect(chat_input(page)).to_contain_text("FOXTROT")
133+
134+
135+
def test_identical_texts_show_no_differences(page_for, make_user):
136+
page = page_for(make_user())
137+
same = "The harbour closes at dusk."
138+
_type_draft(page, same)
139+
compare = _open_compare(page)
140+
_write(compare, "Proposed revision", same)
141+
compare.get_by_role("button", name="Compare", exact=True).click()
142+
143+
expect(compare.get_by_role("status")).to_have_text("No differences")
144+
expect(compare.get_by_role("button", name="Previous change")).to_be_disabled()
145+
expect(compare.get_by_role("button", name="Next change")).to_be_disabled()
146+
expect(compare.get_by_role("button", name="Copy change to proposal")).to_be_disabled()
147+
148+
149+
def test_closing_after_an_edit_asks_to_discard_and_keeps_the_message(page_for, make_user):
150+
page = page_for(make_user())
151+
draft = f"Keep this draft {uuid.uuid4().hex[:6]}"
152+
_type_draft(page, draft)
153+
154+
compare = _open_compare(page)
155+
compare.get_by_role("button", name="Cancel", exact=True).click()
156+
expect(compare).to_have_count(0)
157+
158+
compare = _open_compare(page)
159+
_write(compare, "Proposed revision", "Something else entirely")
160+
compare.get_by_role("button", name="Cancel", exact=True).click()
161+
confirm = page.get_by_role("dialog", name="Discard comparison changes?")
162+
expect(confirm).to_contain_text("Your prompt will stay unchanged.")
163+
confirm.get_by_role("button", name="Cancel").click()
164+
expect(confirm).to_have_count(0)
165+
expect(_pane(compare, "Proposed revision")).to_have_text("Something else entirely")
166+
167+
compare.get_by_role("button", name="Cancel", exact=True).click()
168+
confirm.get_by_role("button", name="Discard").click()
169+
expect(compare).to_have_count(0)
170+
expect(chat_input(page)).to_have_text(draft)

‎e2e/chat/test_slash_commands.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
Typing / lists the chat commands above the saved prompts. Fork copies the open conversation into
44
a new chat that answers from it. `/model <id>` typed and sent switches the chat to that model, so
55
the next message goes there, and an unknown id is refused and leaves the model alone. Model in the
6-
menu opens the model selector, Settings the settings dialog and Temporary turns a new chat
7-
temporary.
6+
menu opens the model selector, Settings the settings dialog, Compare text the compare view with an
7+
empty original once the typed `/compare` is taken out of the message, and Temporary turns a new
8+
chat temporary.
89
910
Discriminates: passes on dev 30f3f6a8f; in a frontend build whose / menu commands and typed /model
10-
do nothing every test fails.
11+
do nothing every test fails. The Compare text test passes on the dev 3b8205505 build and fails in
12+
a build of it whose / menu leaves out Compare text.
1113
"""
1214

1315
from __future__ import annotations
@@ -126,6 +128,17 @@ def test_settings_in_the_menu_opens_the_settings(page_for, make_user):
126128
expect(page.get_by_role("dialog").get_by_role("tab", name="General")).to_be_visible()
127129

128130

131+
def test_compare_text_in_the_menu_opens_the_compare_view(page_for, make_user):
132+
page = page_for(make_user())
133+
menu = _command_menu(page, "compare")
134+
menu.get_by_role("button", name="Compare text").click()
135+
136+
compare = page.get_by_role("dialog", name="Compare text")
137+
expect(compare.get_by_role("textbox", name="Original", exact=True)).to_have_text("")
138+
expect(compare.get_by_role("button", name="Use in prompt")).to_be_visible()
139+
expect(chat_input(page)).to_have_text("")
140+
141+
129142
def test_temporary_in_the_menu_turns_the_new_chat_temporary(page_for, make_user, upstream):
130143
account = make_user()
131144
page = page_for(account)

‎e2e/tools/test_terminal_file_browser_actions.py‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
from the input's Terminal menu and works in the File browser: New Folder and New File in its
55
Actions menu make them in the terminal's home, Rename and Delete in a row's More menu rename the
66
file or remove it after a confirm, and Show Hidden Files lists a dotfile the browser hides by
7-
default. Every change is read back from the terminal's disk.
7+
default. Every change is read back from the terminal's disk. Select for Compare on one file and
8+
Compare with Selected on another open both files side by side, read only, each under its name, and
9+
Swap sides puts the second file on the left.
810
911
Discriminates: passes on dev 30f3f6a8f; on a build whose New Folder, New File, Rename and Delete
1012
handlers send nothing and whose hidden-files switch does not change the listing, every test here
11-
fails.
13+
fails. The compare test passes on the dev 3b8205505 build and fails in a build of it whose Swap
14+
sides leaves the files where they were.
1215
"""
1316

1417
from __future__ import annotations
@@ -113,3 +116,27 @@ def test_show_hidden_files_lists_a_dotfile(browser):
113116

114117
_choose_action(browser, "Hide Hidden Files")
115118
expect(browser.get_by_text(".boat-secrets", exact=True)).to_have_count(0)
119+
120+
121+
def test_compare_with_selected_opens_both_files_and_swap_sides_swaps_them(browser):
122+
_row_menu(browser, "draft-letter.txt").get_by_role("button", name="Select for Compare").click()
123+
expect(browser.get_by_text("Comparing with draft-letter.txt")).to_be_visible()
124+
_row_menu(browser, "old-receipt.txt").get_by_role(
125+
"button", name="Compare with Selected"
126+
).click()
127+
128+
compare = _page(browser).get_by_role("dialog", name="Compare files")
129+
panes = compare.get_by_role("textbox")
130+
expect(panes).to_have_count(2)
131+
expect(panes.first).to_have_accessible_name("draft-letter.txt")
132+
expect(panes.first).to_have_text("Dear harbour master,")
133+
expect(panes.last).to_have_accessible_name("old-receipt.txt")
134+
expect(panes.last).to_have_text("two ropes, one lamp")
135+
expect(panes.first).not_to_be_editable()
136+
expect(compare.get_by_role("status")).to_have_text("Change 1 of 1")
137+
138+
compare.get_by_role("button", name="Swap sides").click()
139+
expect(panes.first).to_have_accessible_name("old-receipt.txt")
140+
expect(panes.first).to_have_text("two ropes, one lamp")
141+
expect(panes.last).to_have_accessible_name("draft-letter.txt")
142+
expect(panes.last).to_have_text("Dear harbour master,")

‎e2e/workspace/test_prompt_versions.py‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
kept for the next comparison; a version that differs only in its line endings says so. Edit as new
99
version makes the old text editable and saves it as a further version that is not production,
1010
leaving what the chat inserts unchanged. Switching versions with unsaved typing asks whether to
11-
discard it first, and so does leaving the editor. Ctrl+S saves the text. A user the prompt is
12-
shared with read only can pick and compare its versions but is offered no way to change them.
11+
discard it first, and so does leaving the editor. Ctrl+S saves the text. A revision written in
12+
the text's Compare revision view and applied to the draft is the text Save stores. A user the
13+
prompt is shared with read only can pick and compare its versions but is offered no way to change
14+
them.
1315
1416
Discriminates: passes on dev b130fec73. In a frontend build where an old version's text box takes
1517
typing the read-only test goes red; where the comparison leaves out the prompt text the comparison
@@ -20,7 +22,9 @@
2022
the chosen layout is not remembered the layout test goes red; and where a change of line endings
2123
alone counts as no change the line endings test goes red. Retargeted for 6a7678ac7, which named the
2224
version picker History and the live version Live: the tests pass on dev 206bf9723, and a frontend
23-
build whose version switch keeps the current text turns the read-only test red.
25+
build whose version switch keeps the current text turns the read-only test red. The Compare
26+
revision test passes on the dev 3b8205505 build and fails in a build of it whose Apply to draft
27+
keeps the original text.
2428
"""
2529

2630
from __future__ import annotations
@@ -210,6 +214,26 @@ def test_ctrl_s_saves_the_prompt_text(page_for, writer):
210214
assert stored["content"] == "Summarise this in one word."
211215

212216

217+
def test_a_revision_applied_from_compare_revision_is_the_saved_text(page_for, writer):
218+
first, _ = _edited_once(writer)
219+
page = page_for(writer)
220+
_open(page, first["id"], ONE_LINE)
221+
revised = "Summarise this in one plain sentence."
222+
223+
page.get_by_role("button", name="Compare revision").click()
224+
compare = page.get_by_role("dialog", name="Compare revision")
225+
expect(compare.get_by_role("textbox", name="Original")).to_have_text(ONE_LINE)
226+
compare.get_by_role("textbox", name="Proposed revision").click()
227+
page.keyboard.insert_text(revised)
228+
compare.get_by_role("button", name="Apply to draft").click()
229+
230+
expect(compare).to_have_count(0)
231+
expect(_content(page)).to_have_value(revised)
232+
page.get_by_role("button", name="Save", exact=True).click()
233+
expect(page.get_by_text("Prompt updated successfully")).to_be_visible()
234+
assert _get(writer, f"/api/v1/prompts/id/{first['id']}")["content"] == revised
235+
236+
213237
def test_leaving_the_editor_with_unsaved_typing_asks_first(page_for, writer):
214238
first, _ = _edited_once(writer)
215239
page = page_for(writer)

‎e2e/workspace/test_skill_file_tree.py‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
Upload is saved with its bytes and shown as a picture. In SKILL.md's preview a link to another
99
file of the skill opens that file. When the skill was saved in another tab meanwhile, Save warns
1010
that a newer version exists and keeps the draft, and Reload latest shows the other tab's work.
11+
A revision written in a file's Compare revision view and applied to the draft is what Save stores
12+
for that file, and the file's Copy puts the open file on the clipboard.
1113
1214
Discriminates: passes on the dev 178de3666 build. In a frontend copy, an editor that sends only
1315
SKILL.md on create turns the create test red; one whose save keeps a renamed file under its old
1416
name as well, or one that does not send deletions, turns the rename and delete test red; an upload
1517
sent as text turns the upload test red; a preview that leaves relative links alone turns the
1618
preview test red; and an editor that does not show a version conflict turns the conflict test
17-
red.
19+
red. The compare and copy tests pass on the dev 3b8205505 build; a build of it whose Apply to draft
20+
keeps the original turns the compare test red, and one whose Copy copies the file's saved content
21+
turns the copy test red.
1822
"""
1923

2024
from __future__ import annotations
@@ -183,3 +187,45 @@ def test_a_save_after_another_tab_saved_keeps_the_draft_and_offers_the_latest(pa
183187
open_row(page, "tables.md")
184188
expect(code_editor(page)).to_contain_text("From the other tab.")
185189
assert current(owner, skill["id"])["version_id"] == other.json()["version_id"]
190+
191+
192+
def _open_tables(page: Page, skill: dict) -> None:
193+
_open_skill(page, skill["id"])
194+
open_row(page, "references")
195+
open_row(page, "tables.md")
196+
expect(page.get_by_title("references/tables.md", exact=True)).to_be_visible()
197+
expect(code_editor(page)).to_contain_text("High water at noon.")
198+
199+
200+
def test_a_revision_applied_from_compare_revision_is_saved_in_the_file(page_for, owner):
201+
skill = _tide_skill(owner)
202+
page = page_for(owner)
203+
_open_tables(page, skill)
204+
205+
page.get_by_role("main").get_by_role("button", name="Compare revision").click()
206+
compare = page.get_by_role("dialog", name="Compare revision")
207+
expect(compare.get_by_role("textbox", name="Original")).to_have_text("High water at noon.")
208+
compare.get_by_role("textbox", name="Proposed revision").click()
209+
page.keyboard.insert_text("High water at two.\n")
210+
compare.get_by_role("button", name="Apply to draft").click()
211+
212+
expect(compare).to_have_count(0)
213+
expect(code_editor(page)).to_contain_text("High water at two.")
214+
_save(page)
215+
expect(page.get_by_text("Skill updated successfully")).to_be_visible()
216+
assert read(owner, skill["id"], "references/tables.md") == b"High water at two.\n"
217+
assert b"Read [the tables]" in read(owner, skill["id"], "SKILL.md")
218+
219+
220+
def test_copy_puts_the_open_file_on_the_clipboard(page_for, owner):
221+
skill = _tide_skill(owner)
222+
page = page_for(owner)
223+
page.context.grant_permissions(["clipboard-read", "clipboard-write"])
224+
_open_tables(page, skill)
225+
replace_text(page, "High water at three.\n")
226+
227+
page.get_by_role("main").get_by_role("button", name="Copy content").click()
228+
229+
expect(page.get_by_text("Copied to clipboard")).to_be_visible()
230+
copied = page.evaluate("() => navigator.clipboard.readText()")
231+
assert copied == "High water at three.\n", f"Copy put {copied!r} on the clipboard"

0 commit comments

Comments
 (0)