init
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
xmpp/
|
||||
xmpp-src/
|
||||
shots/
|
||||
data/
|
||||
server.log
|
||||
server.pid
|
||||
*.out
|
||||
@@ -0,0 +1,20 @@
|
||||
# End-to-end tests
|
||||
|
||||
They drive a running movim-rs (and, for the `browser_*` ones, the real Movim
|
||||
frontend in a headless Chromium) against a local Prosody.
|
||||
|
||||
./setup.sh # once: builds Prosody locally, creates alice, bob and carol
|
||||
cargo build && ./run.sh # starts movim-rs on 127.0.0.1:8099
|
||||
uv run --with websockets --with requests python e2e.py
|
||||
uv run --with websockets --with requests python e2e_muc.py
|
||||
uv run --with websockets --with requests python setup_roster.py # alice and bob become contacts
|
||||
uv run --with playwright python -m playwright install chromium
|
||||
uv run --with playwright python browser_smoke.py # also browser_call, browser_omemo, ...
|
||||
uv run --with playwright python browser_muji.py # 3-way group call (--video for video)
|
||||
uv run --with playwright python browser_preview.py # link previews (serves pages on :8765)
|
||||
|
||||
Screenshots are written to `shots/`.
|
||||
|
||||
On a fresh database the first login replays the whole archive (MAM), so the
|
||||
scripts match on the ids of the messages they send rather than on the first
|
||||
message that shows up. The Python clients send no timezone, so times are UTC.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,66 @@
|
||||
"""Drive the real Movim frontend served by movim-rs with a headless Chromium."""
|
||||
import asyncio, sys, os
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
BASE = "http://127.0.0.1:8099"
|
||||
OUT = os.path.dirname(os.path.abspath(__file__)) + "/shots"
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
|
||||
async def login(browser, jid, password, name):
|
||||
ctx = await browser.new_context(viewport={"width": 1400, "height": 900})
|
||||
page = await ctx.new_page()
|
||||
errors = []
|
||||
page.on("pageerror", lambda e: errors.append(f"[{name}] pageerror: {e} {e.stack[:600] if e.stack else ''}"))
|
||||
page.on("console", lambda m: m.type == "error" and errors.append(f"[{name}] console: {m.text}"))
|
||||
await page.goto(BASE + "/login")
|
||||
await page.wait_for_selector("#login_widget input[type=submit]:not([disabled])", timeout=15000)
|
||||
await page.fill("input#username", jid)
|
||||
await page.fill("input#password", password)
|
||||
await page.click("#login_widget input[type=submit]")
|
||||
await page.wait_for_url("**/chat**", timeout=30000)
|
||||
await page.wait_for_timeout(3000)
|
||||
await page.screenshot(path=f"{OUT}/{name}_1_chat.png")
|
||||
return ctx, page, errors
|
||||
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
actx, alice, aerr = await login(browser, "alice@localhost", "alicepass", "alice")
|
||||
bctx, bob, berr = await login(browser, "bob@localhost", "bobpass", "bob")
|
||||
|
||||
# Alice opens the discussion with Bob
|
||||
await alice.goto(BASE + "/chat/bob@localhost")
|
||||
await alice.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await alice.wait_for_timeout(2000)
|
||||
await alice.fill("#chat_textarea", "Hello from the browser 🙂")
|
||||
await alice.press("#chat_textarea", "Enter")
|
||||
await alice.wait_for_timeout(2000)
|
||||
await alice.screenshot(path=f"{OUT}/alice_2_sent.png")
|
||||
|
||||
# Bob sees the chat in the list and opens it
|
||||
await bob.wait_for_timeout(1000)
|
||||
await bob.screenshot(path=f"{OUT}/bob_2_list.png")
|
||||
await bob.goto(BASE + "/chat/alice@localhost")
|
||||
await bob.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await bob.wait_for_timeout(2500)
|
||||
await bob.screenshot(path=f"{OUT}/bob_3_chat.png")
|
||||
|
||||
bubbles = await bob.locator("#chat_widget .bubble p").all_inner_texts()
|
||||
print("bob bubbles:", bubbles[-5:])
|
||||
|
||||
# Mobile layout
|
||||
mctx = await browser.new_context(viewport={"width": 400, "height": 800}, storage_state=await bctx.storage_state())
|
||||
mobile = await mctx.new_page()
|
||||
await mobile.goto(BASE + "/chat")
|
||||
await mobile.wait_for_timeout(3000)
|
||||
await mobile.screenshot(path=f"{OUT}/bob_4_mobile.png")
|
||||
|
||||
for e in aerr + berr:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(args=["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"])
|
||||
actx, alice, aerr = await login(browser, "alice@localhost", "alicepass", "callalice")
|
||||
bctx, bob, berr = await login(browser, "bob@localhost", "bobpass", "callbob")
|
||||
for c in (actx, bctx):
|
||||
await c.grant_permissions(["microphone", "camera"], origin=BASE)
|
||||
await alice.goto(BASE + "/chat/bob@localhost")
|
||||
await alice.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await alice.wait_for_timeout(3000)
|
||||
await alice.screenshot(path=f"{OUT}/call_0_header.png")
|
||||
btn = alice.locator("#chat_header span.control.icon[title]").first
|
||||
print("call buttons:", await alice.locator("#chat_header span.control.icon[title]").count())
|
||||
await btn.click()
|
||||
await alice.wait_for_selector("#lobby_start:not(.disabled)", timeout=15000)
|
||||
await alice.screenshot(path=f"{OUT}/call_1_lobby.png")
|
||||
await alice.click("#lobby_start")
|
||||
await bob.wait_for_selector("#lobby_start", timeout=15000)
|
||||
await bob.wait_for_timeout(1500)
|
||||
await bob.screenshot(path=f"{OUT}/call_2_incoming.png")
|
||||
await bob.wait_for_selector("#lobby_start:not(.disabled)", timeout=15000)
|
||||
await bob.click("#lobby_start")
|
||||
await alice.wait_for_timeout(6000)
|
||||
await alice.screenshot(path=f"{OUT}/call_3_alice.png")
|
||||
await bob.screenshot(path=f"{OUT}/call_3_bob.png")
|
||||
states = await alice.evaluate("Object.values(MovimJingles.sessions || {}).map(s => s.pc && s.pc.connectionState)")
|
||||
print("alice peer connection states:", states)
|
||||
# Hang up
|
||||
await alice.evaluate("Visio_ajaxGoodbye(document.querySelector('#visio').dataset.jid || 'bob@localhost', Object.keys(MovimJingles.sessions)[0] ? MovimJingles.sessions[Object.keys(MovimJingles.sessions)[0]].id : '')")
|
||||
await alice.wait_for_timeout(3000)
|
||||
await bob.screenshot(path=f"{OUT}/call_4_bob_end.png")
|
||||
for e in aerr + berr:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
import asyncio, time
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
# Account creation
|
||||
ctx = await browser.new_context(viewport={"width": 1200, "height": 900})
|
||||
page = await ctx.new_page()
|
||||
errors = []
|
||||
page.on("pageerror", lambda e: errors.append(f"pageerror: {e}"))
|
||||
await page.goto(BASE + "/account")
|
||||
await page.wait_for_timeout(1000)
|
||||
await page.screenshot(path=f"{OUT}/reg_1_account.png")
|
||||
await page.click("#subscribe li.block")
|
||||
await page.wait_for_selector("form[name=data] input[name=username]", timeout=15000)
|
||||
await page.screenshot(path=f"{OUT}/reg_2_form.png")
|
||||
user = f"carol{int(time.time())}"
|
||||
await page.fill("form[name=data] input[name=username]", user)
|
||||
await page.fill("form[name=data] input[name=password]", "carolpass")
|
||||
await page.click("form[name=data] button")
|
||||
await page.wait_for_timeout(2500)
|
||||
await page.screenshot(path=f"{OUT}/reg_3_done.png")
|
||||
print("registered", user, "->", await page.locator("#subscribe .placeholder h4").all_inner_texts())
|
||||
|
||||
# Configuration page
|
||||
ctx2, page2, errors2 = await login(browser, f"{user}@localhost", "carolpass", "carol")
|
||||
await page2.goto(BASE + "/configuration")
|
||||
await page2.wait_for_timeout(3000)
|
||||
await page2.screenshot(path=f"{OUT}/config_1.png")
|
||||
for e in errors + errors2:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
ctx, page, errors = await login(browser, "alice@localhost", "alicepass", "logout")
|
||||
await page.evaluate("Presence_ajaxAskLogout()")
|
||||
await page.wait_for_timeout(800)
|
||||
await page.click("#dialog footer button[name=submit]")
|
||||
await page.wait_for_url("**/login**", timeout=15000)
|
||||
await page.wait_for_timeout(2000)
|
||||
print("after logout:", page.url, "quick login keys:", await page.evaluate("localStorage.getItem('quickDeviceId')"))
|
||||
await page.screenshot(path=f"{OUT}/logout.png")
|
||||
# Login again, then reload: quick login must work after a server restart
|
||||
await page.fill("input#username", "alice@localhost")
|
||||
await page.fill("input#password", "alicepass")
|
||||
await page.click("#login_widget input[type=submit]")
|
||||
await page.wait_for_url("**/chat**", timeout=30000)
|
||||
print("logged again:", page.url)
|
||||
import subprocess
|
||||
subprocess.run([os.path.join(os.path.dirname(os.path.abspath(__file__)), "run.sh")])
|
||||
await page.wait_for_timeout(1000)
|
||||
await page.reload()
|
||||
await page.wait_for_url("**/chat**", timeout=40000)
|
||||
await page.wait_for_timeout(8000)
|
||||
print("after server restart:", page.url)
|
||||
await page.screenshot(path=f"{OUT}/restart.png")
|
||||
for e in errors:
|
||||
print(e[:200])
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,34 @@
|
||||
import asyncio, os
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
ctx, page, errors = await login(browser, "bob@localhost", "bobpass", "bobmuc")
|
||||
await page.wait_for_timeout(3000)
|
||||
await page.screenshot(path=f"{OUT}/muc_1_list.png")
|
||||
room = page.locator("#rooms ul.list.rooms li.room").first
|
||||
await room.click()
|
||||
await page.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await page.wait_for_timeout(3000)
|
||||
await page.fill("#chat_textarea", "Hi everyone @alice")
|
||||
await page.press("#chat_textarea", "Enter")
|
||||
await page.wait_for_timeout(2500)
|
||||
await page.screenshot(path=f"{OUT}/muc_2_room.png")
|
||||
# Room drawer
|
||||
await page.click("#chat_widget header span.primary.icon.bubble")
|
||||
await page.wait_for_timeout(1500)
|
||||
await page.screenshot(path=f"{OUT}/muc_3_drawer.png")
|
||||
# Search drawer (new chat)
|
||||
await page.keyboard.press("Escape")
|
||||
await page.goto(BASE + "/chat")
|
||||
await page.wait_for_timeout(2000)
|
||||
await page.click("main div[role=menu] > button.action")
|
||||
await page.wait_for_timeout(2000)
|
||||
await page.screenshot(path=f"{OUT}/muc_4_search.png")
|
||||
for e in errors:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Muji (XEP-0272) group call between three browsers in a group chat."""
|
||||
import asyncio, time
|
||||
from playwright.async_api import async_playwright
|
||||
import sys
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
VIDEO = 'true' if '--video' in sys.argv else 'false'
|
||||
|
||||
|
||||
async def open_room(page, room):
|
||||
await page.evaluate(f"Chat_ajaxGetRoom('{room}')")
|
||||
await page.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await page.wait_for_timeout(1500)
|
||||
|
||||
|
||||
async def join_from_lobby(page):
|
||||
await page.wait_for_selector("#lobby_start:not(.disabled)", timeout=15000)
|
||||
await page.click("#lobby_start")
|
||||
|
||||
|
||||
async def sessions(page):
|
||||
return await page.evaluate(
|
||||
"Object.fromEntries(Object.entries(MovimJingles.sessions || {}).map(([j, s]) => [j, s.pc && s.pc.connectionState]))"
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
room = f"call{int(time.time())}@conference.localhost"
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(args=["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"])
|
||||
users = [("alice", "alicepass"), ("bob", "bobpass"), ("carol", "carolpass")]
|
||||
pages, errors = {}, []
|
||||
for name, password in users:
|
||||
ctx, page, err = await login(browser, f"{name}@localhost", password, f"muji{name}")
|
||||
await ctx.grant_permissions(["microphone", "camera"], origin=BASE)
|
||||
pages[name] = page
|
||||
await page.evaluate("""
|
||||
window.addEventListener('unhandledrejection', e => console.error('REJECTION', e.reason && e.reason.stack));
|
||||
window.addEventListener('error', e => console.error('ERROR', e.error && e.error.stack));
|
||||
""")
|
||||
errors.append(err)
|
||||
alice, bob, carol = pages["alice"], pages["bob"], pages["carol"]
|
||||
|
||||
# Alice creates a group chat and invites the others, who join it
|
||||
await alice.evaluate(f"""RoomsUtils_ajaxAddCreate({{
|
||||
jid: {{value: '{room}'}}, name: {{value: 'Call room'}}, nick: {{value: 'alice'}},
|
||||
type: {{value: 'groupchat'}}, autojoin: {{value: true}}, pinned: {{value: false}},
|
||||
notify: {{value: 'always'}}
|
||||
}})""")
|
||||
await alice.wait_for_timeout(3000)
|
||||
for name in ("bob", "carol"):
|
||||
await alice.evaluate(f"RoomsUtils_ajaxInvite({{to: {{value: '{room}'}}, invite: {{value: '{name}@localhost'}}}})")
|
||||
await alice.wait_for_timeout(1500)
|
||||
for name in ("bob", "carol"):
|
||||
await pages[name].evaluate(f"""RoomsUtils_ajaxAddConfirm({{
|
||||
jid: {{value: '{room}'}}, name: {{value: 'Call room'}}, nick: {{value: '{name}'}},
|
||||
autojoin: {{value: true}}, pinned: {{value: false}}, notify: {{value: 'on-mention'}}
|
||||
}})""")
|
||||
await alice.wait_for_timeout(3000)
|
||||
for page in pages.values():
|
||||
await open_room(page, room)
|
||||
await alice.screenshot(path=f"{OUT}/muji_0_header.png")
|
||||
|
||||
# Alice starts the call
|
||||
await alice.evaluate(f"Visio_ajaxGetMujiLobby('{room}', true, {VIDEO})")
|
||||
await join_from_lobby(alice)
|
||||
await alice.wait_for_timeout(3000)
|
||||
await alice.screenshot(path=f"{OUT}/muji_1_alice_waiting.png")
|
||||
await bob.screenshot(path=f"{OUT}/muji_1_bob_header.png")
|
||||
print("bob sees a call to join:", await bob.locator("button.oppose.color.blue").count() > 0)
|
||||
|
||||
# Bob then Carol join
|
||||
for name, page in (("bob", bob), ("carol", carol)):
|
||||
button = page.locator("button.oppose.color.blue").first
|
||||
try:
|
||||
await button.click(timeout=10000)
|
||||
except Exception:
|
||||
await page.screenshot(path=f"{OUT}/muji_fail_{name}.png")
|
||||
print(name, "header:", await page.content())
|
||||
raise
|
||||
await join_from_lobby(page)
|
||||
await page.wait_for_timeout(6000)
|
||||
print(f"after {name} joined:", {n: await sessions(pg) for n, pg in pages.items()})
|
||||
|
||||
for name, page in pages.items():
|
||||
await page.screenshot(path=f"{OUT}/muji_2_{name}.png")
|
||||
print(name, "sessions:", await sessions(page))
|
||||
|
||||
print("JS errors before leaving:", sum(len(e) for e in errors))
|
||||
# Carol leaves, the others keep talking
|
||||
await carol.evaluate(f"Visio_ajaxLeaveMuji('{room}')")
|
||||
await alice.wait_for_timeout(3000)
|
||||
print("after carol left, alice sessions:", await sessions(alice))
|
||||
print("carol sessions:", await sessions(carol))
|
||||
await alice.screenshot(path=f"{OUT}/muji_3_alice.png")
|
||||
|
||||
for err in errors:
|
||||
for e in err:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
actx, alice, aerr = await login(browser, "alice@localhost", "alicepass", "omemoalice")
|
||||
bctx, bob, berr = await login(browser, "bob@localhost", "bobpass", "omemobob")
|
||||
# Let both browsers generate and publish their bundles
|
||||
await alice.wait_for_timeout(5000)
|
||||
await bob.wait_for_timeout(1000)
|
||||
print("alice device:", await alice.evaluate("new ChatOmemoStorage().getLocalRegistrationId()"))
|
||||
print("bob device:", await bob.evaluate("new ChatOmemoStorage().getLocalRegistrationId()"))
|
||||
|
||||
await alice.goto(BASE + "/chat/bob@localhost")
|
||||
await alice.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await alice.wait_for_timeout(2000)
|
||||
print("alice state before:", await alice.evaluate("Chat.getOmemoState()"))
|
||||
await alice.evaluate("ChatOmemo.enableContactState('bob@localhost', false)")
|
||||
await alice.wait_for_timeout(5000)
|
||||
print("alice state after:", await alice.evaluate("Chat.getOmemoState()"))
|
||||
await alice.fill("#chat_textarea", "This is a secret 🔐")
|
||||
await alice.press("#chat_textarea", "Enter")
|
||||
await alice.wait_for_timeout(3000)
|
||||
await alice.screenshot(path=f"{OUT}/omemo_1_alice.png")
|
||||
|
||||
await bob.goto(BASE + "/chat/alice@localhost")
|
||||
await bob.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await bob.wait_for_timeout(5000)
|
||||
await bob.screenshot(path=f"{OUT}/omemo_2_bob.png")
|
||||
texts = await bob.locator("#chat_widget .bubble p").all_inner_texts()
|
||||
print("bob last bubbles:", texts[-3:])
|
||||
for e in aerr + berr:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Link previews: a page with OpenGraph tags and a direct picture link."""
|
||||
import asyncio, functools, http.server, os, shutil, tempfile, threading
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PORT = 8765
|
||||
PAGE = """<!doctype html><html><head>
|
||||
<title>Fallback title</title>
|
||||
<meta property="og:title" content="A very interesting article">
|
||||
<meta property="og:description" content="Everything you always wanted to know about link previews.">
|
||||
<meta property="og:image" content="/test.png">
|
||||
<meta property="og:site_name" content="Example News">
|
||||
<link rel="icon" href="/test.png">
|
||||
</head><body>Hello</body></html>"""
|
||||
|
||||
|
||||
def serve():
|
||||
root = tempfile.mkdtemp()
|
||||
with open(os.path.join(root, "article.html"), "w") as f:
|
||||
f.write(PAGE)
|
||||
shutil.copy(os.path.join(HERE, "test.png"), os.path.join(root, "test.png"))
|
||||
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=root)
|
||||
server = http.server.ThreadingHTTPServer(("127.0.0.1", PORT), handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
return server
|
||||
|
||||
|
||||
async def main():
|
||||
server = serve()
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
_, alice, aerr = await login(browser, "alice@localhost", "alicepass", "previewalice")
|
||||
_, bob, berr = await login(browser, "bob@localhost", "bobpass", "previewbob")
|
||||
|
||||
await alice.goto(BASE + "/chat/bob@localhost")
|
||||
await alice.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await bob.goto(BASE + "/chat/alice@localhost")
|
||||
await bob.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await bob.wait_for_timeout(1500)
|
||||
|
||||
for text in (f"Have a look http://127.0.0.1:{PORT}/article.html", f"http://127.0.0.1:{PORT}/test.png"):
|
||||
await bob.fill("#chat_textarea", text)
|
||||
await bob.press("#chat_textarea", "Enter")
|
||||
await bob.wait_for_timeout(1500)
|
||||
await alice.wait_for_timeout(4000)
|
||||
|
||||
for name, page in (("alice", alice), ("bob", bob)):
|
||||
await page.screenshot(path=f"{OUT}/preview_{name}.png")
|
||||
cards = await page.locator("#chat_widget ul.list.card").all_inner_texts()
|
||||
pictures = await page.locator("#chat_widget div.bubble img").count()
|
||||
print(name, "cards:", [c.replace("\n", " | ")[:120] for c in cards[-2:]], "pictures:", pictures)
|
||||
|
||||
for e in aerr + berr:
|
||||
print(e)
|
||||
await browser.close()
|
||||
server.shutdown()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,50 @@
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
|
||||
STEPS = [
|
||||
("contact drawer", "ChatActions_ajaxGetContact('bob@localhost')"),
|
||||
("search drawer", "Search_ajaxRequest()"),
|
||||
("notifications drawer", "Notifications_ajaxRequest()"),
|
||||
("presence menu", "Presence_ajaxHttpMenu()"),
|
||||
("emoji picker", "Stickers_ajaxReaction(null)"),
|
||||
("chat search", "ChatActions_ajaxShowSearchDialog('bob@localhost', false)"),
|
||||
("add contact dialog", "Notifications_ajaxAddAsk('carol@localhost')"),
|
||||
("room discover", "RoomsUtils_ajaxHttpRoomDiscover()"),
|
||||
("create room", "RoomsUtils_ajaxAdd(null, null, true)"),
|
||||
("clear history dialog", "Chat_ajaxClearHistory('bob@localhost')"),
|
||||
("logout dialog", "Presence_ajaxAskLogout()"),
|
||||
("upload dialog", "Upload_ajaxGetPanel()"),
|
||||
]
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
ctx, page, errors = await login(browser, "alice@localhost", "alicepass", "smoke")
|
||||
await page.goto(BASE + "/chat/bob@localhost")
|
||||
await page.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await page.wait_for_timeout(2000)
|
||||
# message dialog on the last message
|
||||
mid = await page.evaluate("document.querySelector('#chat_widget .message[data-mid]:last-of-type')?.dataset.mid")
|
||||
steps = STEPS + ([("message dialog", f"ChatActions_ajaxShowMessageDialog({mid})")] if mid else [])
|
||||
for i, (name, js) in enumerate(steps):
|
||||
before = len(errors)
|
||||
await page.evaluate(js)
|
||||
await page.wait_for_timeout(1200)
|
||||
await page.screenshot(path=f"{OUT}/smoke_{i:02d}.png")
|
||||
print(f"{name}: {'ERR ' + errors[-1][:150] if len(errors) > before else 'ok'}")
|
||||
await page.evaluate("Dialog_ajaxClear(); Drawer_ajaxClear(); document.querySelector('#drawer')?.classList.add('empty')")
|
||||
await page.wait_for_timeout(300)
|
||||
# configuration tabs
|
||||
await page.goto(BASE + "/configuration")
|
||||
await page.wait_for_timeout(2500)
|
||||
for tab in await page.locator("#navtabs li").all():
|
||||
await tab.click()
|
||||
await page.wait_for_timeout(700)
|
||||
await page.screenshot(path=f"{OUT}/smoke_config_last.png")
|
||||
print("errors total:", len(errors))
|
||||
for e in errors[:10]:
|
||||
print(e[:300])
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
from browser import login, BASE, OUT
|
||||
import os
|
||||
S = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
ctx, page, errors = await login(browser, "alice@localhost", "alicepass", "aliceup")
|
||||
await page.goto(BASE + "/chat/bob@localhost")
|
||||
await page.wait_for_selector("#chat_textarea", timeout=15000)
|
||||
await page.wait_for_timeout(2000)
|
||||
for f in ("test.png", "notes.txt"):
|
||||
await page.evaluate("Upload_ajaxGetPanel()")
|
||||
await page.wait_for_selector("#upload", timeout=10000)
|
||||
await page.set_input_files("input#file", f"{S}/{f}")
|
||||
await page.wait_for_timeout(1500)
|
||||
await page.click("#upload_button")
|
||||
await page.wait_for_timeout(4000)
|
||||
await page.screenshot(path=f"{OUT}/upload_1.png")
|
||||
for e in errors:
|
||||
print(e)
|
||||
await browser.close()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,138 @@
|
||||
import uuid
|
||||
"""End-to-end test of movim-rs, simulating what the browser does."""
|
||||
import asyncio, json, sys, re
|
||||
import requests, websockets
|
||||
|
||||
BASE = "http://127.0.0.1:8099"
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, jid, password):
|
||||
self.jid, self.password = jid, password
|
||||
self.http = requests.Session()
|
||||
self.ws = None
|
||||
self.received = []
|
||||
|
||||
def cookie(self):
|
||||
return self.http.cookies.get("MOVIM_SESSION_ID")
|
||||
|
||||
async def connect_ws(self):
|
||||
self.ws = await websockets.connect(
|
||||
BASE.replace("http", "ws") + "/ws/",
|
||||
additional_headers={"Cookie": f"MOVIM_SESSION_ID={self.cookie()}", "Origin": BASE},
|
||||
)
|
||||
|
||||
async def recv_until(self, pred, timeout=15, seen=False):
|
||||
if seen:
|
||||
for m in self.received:
|
||||
if pred(m):
|
||||
return m
|
||||
end = asyncio.get_event_loop().time() + timeout
|
||||
while True:
|
||||
left = end - asyncio.get_event_loop().time()
|
||||
if left <= 0:
|
||||
raise TimeoutError("timeout waiting")
|
||||
msg = json.loads(await asyncio.wait_for(self.ws.recv(), left))
|
||||
self.received.append(msg)
|
||||
if pred(msg):
|
||||
return msg
|
||||
|
||||
async def call(self, widget, func, params=None):
|
||||
body = {"w": widget, "f": func}
|
||||
if params is not None:
|
||||
body["p"] = params
|
||||
await self.ws.send(json.dumps({"func": "message", "b": body}))
|
||||
|
||||
def ajax(self, widget, func, params=None, page="chat", daemon=False):
|
||||
body = {"c": page, "w": widget, "f": func}
|
||||
if params is not None:
|
||||
body["p"] = params
|
||||
r = self.http.post(BASE + ("/ajaxd" if daemon else "/ajax"), json={"func": "message", "b": body})
|
||||
return r
|
||||
|
||||
async def login(self):
|
||||
r = self.http.get(BASE + "/login")
|
||||
assert r.status_code == 200, r.status_code
|
||||
await self.connect_ws()
|
||||
await self.recv_until(lambda m: m.get("func") == "started")
|
||||
await self.ws.send(json.dumps({"func": "register", "host": self.jid.split("@")[1]}))
|
||||
await self.recv_until(lambda m: m.get("func") == "registered")
|
||||
form = {"username": {"value": self.jid}, "password": {"value": self.password}}
|
||||
await self.call("Login", "ajaxLogin", [form, self.cookie(), "Europe/Paris"])
|
||||
msg = await self.recv_until(lambda m: m.get("func") in ("MovimUtils.reloadThis", "MovimTpl.fill", "Login.setCookie") and (m.get("func") != "MovimTpl.fill" or m["p"][0] == "#error"), timeout=30)
|
||||
if msg["func"] == "Login.setCookie":
|
||||
# An existing session is shared
|
||||
self.http.cookies.set("MOVIM_SESSION_ID", msg["p"][0], domain="127.0.0.1")
|
||||
elif msg["func"] != "MovimUtils.reloadThis":
|
||||
raise RuntimeError("login failed: " + re.sub(r"\s+", " ", msg["p"][1])[:300])
|
||||
await self.ws.close()
|
||||
# Reload the page like the browser would
|
||||
r = self.http.get(BASE + "/chat")
|
||||
assert r.status_code == 200 and "chat_widget" in r.text, (r.status_code, r.url)
|
||||
await self.connect_ws()
|
||||
|
||||
|
||||
async def main():
|
||||
alice = Client("alice@localhost", "alicepass")
|
||||
bob = Client("bob@localhost", "bobpass")
|
||||
await alice.login()
|
||||
print("alice logged in")
|
||||
await bob.login()
|
||||
print("bob logged in")
|
||||
# On a fresh database the first login replays the archive (MAM): let it settle
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# Same calls as the chat page on load
|
||||
for c in (alice, bob):
|
||||
await c.call("Chat", "ajaxInit")
|
||||
await c.call("Chats", "ajaxHttpGet")
|
||||
await c.call("Rooms", "ajaxHttpGet")
|
||||
await c.call("Presence", "ajaxHttpGetPresence")
|
||||
await alice.recv_until(lambda m: m.get("func") == "Chat.init")
|
||||
print("init ok")
|
||||
|
||||
# Alice opens a chat with Bob and sends a message
|
||||
await alice.call("Chat", "ajaxGet", ["bob@localhost"])
|
||||
await alice.recv_until(lambda m: m.get("func") == "Chat.setSpecificElements")
|
||||
msgid = str(uuid.uuid4())
|
||||
r = alice.ajax("Chat", "ajaxHttpDaemonSendMessage", [msgid, "bob@localhost", "Hello *Bob* 👋 https://movim.eu", False], daemon=True)
|
||||
assert r.status_code == 200, r.status_code
|
||||
# Match on our message id: older messages may be replayed from the archive
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and msgid in json.dumps(m))
|
||||
print("alice sees her message:", json.dumps(m["p"][0], ensure_ascii=False)[:400])
|
||||
|
||||
# Bob receives it
|
||||
m = await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and msgid in json.dumps(m))
|
||||
msg = next(v for p in m["p"][0].values() for v in p.values() if msgid in json.dumps(v))
|
||||
print("bob receives:", msg["body"][:200], "| from", msg["jidfrom"])
|
||||
await bob.recv_until(lambda m: m.get("func") == "Notif.desktop", seen=True)
|
||||
print("bob notified")
|
||||
|
||||
# Bob opens the discussion and the receipt/displayed flow
|
||||
await bob.call("Notif", "ajaxCurrent", ["chat|alice@localhost"])
|
||||
await bob.call("Chat", "ajaxGet", ["alice@localhost"])
|
||||
await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper")
|
||||
await bob.call("Chat", "ajaxDisplayed", ["alice@localhost", msg["id"]])
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and any(v.get("displayed") for p in m["p"][0].values() for v in p.values()))
|
||||
print("alice sees displayed marker")
|
||||
|
||||
# Bob reacts
|
||||
r = bob.ajax("Chat", "ajaxHttpDaemonSendReaction", [msg["mid"], "👍"], daemon=True)
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and any(v.get("reactionsHtml") for p in m["p"][0].values() for v in p.values()))
|
||||
print("alice sees the reaction")
|
||||
|
||||
# Chat states
|
||||
await bob.call("Chat", "ajaxSendComposing", ["alice@localhost", False])
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "MovimTpl.fill" and m["p"][0].endswith("-state") and m["p"][1] != "")
|
||||
print("alice sees bob composing")
|
||||
|
||||
# Bob replies, Alice corrects her message
|
||||
r = bob.ajax("Chat", "ajaxHttpDaemonSendMessage", [str(uuid.uuid4()), "alice@localhost", "Hi Alice", False, None, msg["mid"]], daemon=True)
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and any(v.get("parent") for p in m["p"][0].values() for v in p.values()))
|
||||
print("alice receives the reply with its parent")
|
||||
|
||||
print("ALL OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,70 @@
|
||||
import uuid
|
||||
"""MUC end-to-end test."""
|
||||
import asyncio, json, time
|
||||
from e2e import Client
|
||||
|
||||
|
||||
async def main():
|
||||
alice = Client("alice@localhost", "alicepass")
|
||||
bob = Client("bob@localhost", "bobpass")
|
||||
await alice.login()
|
||||
await bob.login()
|
||||
for c in (alice, bob):
|
||||
await c.call("Chat", "ajaxInit")
|
||||
await c.call("Rooms", "ajaxHttpGet")
|
||||
|
||||
room = f"test{int(time.time())}@conference.localhost"
|
||||
form = {
|
||||
"jid": {"value": room},
|
||||
"name": {"value": "Test room"},
|
||||
"nick": {"value": "alice"},
|
||||
"type": {"value": "groupchat"},
|
||||
"autojoin": {"value": True},
|
||||
"pinned": {"value": False},
|
||||
"notify": {"value": "always"},
|
||||
}
|
||||
await alice.call("RoomsUtils", "ajaxAddCreate", [form])
|
||||
await alice.recv_until(lambda m: m.get("func") == "Rooms.setRoom" and "connected" in m["p"][1] and room.split("@")[0] in m["p"][1], timeout=20)
|
||||
print("room created and joined:", room)
|
||||
|
||||
# Invite Bob
|
||||
await alice.call("RoomsUtils", "ajaxInvite", [{"to": {"value": room}, "invite": {"value": "bob@localhost"}}])
|
||||
m = await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and "invitation" in json.dumps(m), timeout=10)
|
||||
print("bob got the invitation")
|
||||
|
||||
# Bob joins and adds the bookmark
|
||||
await bob.call("RoomsUtils", "ajaxAddConfirm", [{
|
||||
"jid": {"value": room}, "name": {"value": "Test room"}, "nick": {"value": "bob"},
|
||||
"autojoin": {"value": True}, "pinned": {"value": True}, "notify": {"value": "on-mention"},
|
||||
}])
|
||||
await bob.recv_until(lambda m: m.get("func") == "Rooms.setRoom" and "connected" in m["p"][1], timeout=20)
|
||||
print("bob joined")
|
||||
|
||||
# Alice sends a message, Bob opens the room
|
||||
await alice.call("Chat", "ajaxGetRoom", [room])
|
||||
await alice.recv_until(lambda m: m.get("func") == "Chat.setSpecificElements")
|
||||
r = alice.ajax("Chat", "ajaxHttpDaemonSendMessage", [str(uuid.uuid4()), room, "Hello bob in the room", True], daemon=True)
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and "Hello bob in the room" in json.dumps(m))
|
||||
msg = list(list(m["p"][0].values())[0].values())[0]
|
||||
print("alice sees her reflected MUC message, mine =", msg.get("mine"), "stanzaid =", msg.get("stanzaid"))
|
||||
m = await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and "Hello bob in the room" in json.dumps(m))
|
||||
print("bob receives the MUC message")
|
||||
m = await bob.recv_until(lambda m: m.get("func") == "Notif.desktop", seen=True, timeout=5)
|
||||
print("bob notified (mention)")
|
||||
|
||||
# Bob opens the room: history from the DB
|
||||
await bob.call("Chat", "ajaxGetRoom", [room])
|
||||
m = await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and "Hello bob in the room" in json.dumps(m))
|
||||
await bob.call("Chat", "ajaxHttpGetPresences", [room])
|
||||
m = await bob.recv_until(lambda m: m.get("func") == "MovimTpl.fill" and m["p"][0].endswith("-nav"))
|
||||
print("bob sees the members:", "alice" in m["p"][1] and "bob" in m["p"][1])
|
||||
|
||||
# Reaction in the MUC
|
||||
r = bob.ajax("Chat", "ajaxHttpDaemonSendReaction", [msg["mid"] if msg.get("mid") else 0, "❤️"], daemon=True)
|
||||
|
||||
# Subject
|
||||
await alice.call("RoomsUtils", "ajaxSetSubject", [room, {"subject": {"value": "Our subject"}}])
|
||||
print("ALL OK")
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
import uuid
|
||||
import asyncio, json, subprocess, time
|
||||
from e2e import Client
|
||||
S = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
async def main():
|
||||
alice = Client("alice@localhost", "alicepass"); bob = Client("bob@localhost", "bobpass")
|
||||
await alice.login(); await bob.login()
|
||||
print("logged in, restarting prosody")
|
||||
subprocess.run([f"{S}/prosody.sh", "restart"], capture_output=True)
|
||||
t = time.time()
|
||||
await alice.recv_until(lambda m: m.get("func") == "MovimUtils.addClass" and m["p"][0] == "#status_websocket" and m["p"][1] == "hide", timeout=90)
|
||||
await bob.recv_until(lambda m: m.get("func") == "MovimUtils.addClass" and m["p"][0] == "#status_websocket" and m["p"][1] == "hide", timeout=90)
|
||||
print(f"both reconnected after {time.time()-t:.1f}s")
|
||||
bob.ajax("Chat", "ajaxHttpDaemonSendMessage", [str(uuid.uuid4()), "alice@localhost", "after the restart", False], daemon=True)
|
||||
await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and "after the restart" in json.dumps(m), timeout=15)
|
||||
print("alice receives messages after reconnection: OK")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1 @@
|
||||
hello file
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
D=$(cd "$(dirname "$0")" && pwd)
|
||||
P=$D/xmpp
|
||||
export LUA_PATH="$P/share/lua/5.4/?.lua;$P/share/lua/5.4/?/init.lua;;"
|
||||
export LUA_CPATH="$P/lib/lua/5.4/?.so;;"
|
||||
exec "$P/prosody/bin/prosodyctl" --config "$P/prosody.cfg.lua" "$@"
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# (Re)start movim-rs on 127.0.0.1:8099 against the local Prosody
|
||||
D=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$D/../.." && pwd)
|
||||
[ -f "$D/server.pid" ] && kill "$(cat "$D/server.pid")" 2>/dev/null
|
||||
sleep 0.3
|
||||
cd "$ROOT"
|
||||
MOVIM_BIND=127.0.0.1:8099 MOVIM_DOMAIN=localhost MOVIM_ALLOW_PRIVATE_URLS=1 \
|
||||
MOVIM_XMPP_PLAINTEXT=127.0.0.1:5222 MOVIM_DATA_DIR="$D/data" RUST_LOG=${RUST_LOG:-movim=info} \
|
||||
./target/debug/movim > "$D/server.log" 2>&1 &
|
||||
echo $! > "$D/server.pid"
|
||||
sleep 1
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
# Build a local Prosody test server (no root needed) in tests/e2e/xmpp and
|
||||
# create the test accounts alice/alicepass and bob/bobpass.
|
||||
# Requires: lua 5.4 headers, expat, openssl, curl, gcc, make.
|
||||
set -e
|
||||
D=$(cd "$(dirname "$0")" && pwd)
|
||||
P=$D/xmpp
|
||||
SRC=$D/xmpp-src
|
||||
mkdir -p "$SRC" "$P/lib/lua/5.4" "$P/share/lua/5.4/lxp" "$P/data" "$P/certs"
|
||||
cd "$SRC"
|
||||
[ -d prosody-13.0.7 ] || curl -sL https://prosody.im/downloads/source/prosody-13.0.7.tar.gz | tar xz
|
||||
[ -d luasocket-3.1.0 ] || curl -sL https://github.com/lunarmodules/luasocket/archive/refs/tags/v3.1.0.tar.gz | tar xz
|
||||
[ -d luafilesystem-1_8_0 ] || curl -sL https://github.com/lunarmodules/luafilesystem/archive/refs/tags/v1_8_0.tar.gz | tar xz
|
||||
[ -d luaexpat-1.5.2 ] || curl -sL https://github.com/lunarmodules/luaexpat/archive/refs/tags/1.5.2.tar.gz | tar xz
|
||||
[ -d luasec-1.3.2 ] || curl -sL https://github.com/lunarmodules/luasec/archive/refs/tags/v1.3.2.tar.gz | tar xz
|
||||
|
||||
(cd luasocket-3.1.0 && make LUAV=5.4 LUAINC_linux=/usr/include/lua5.4 linux >/dev/null && make LUAV=5.4 prefix="$P" install >/dev/null)
|
||||
gcc -O2 -fPIC -shared -I/usr/include/lua5.4 luafilesystem-1_8_0/src/lfs.c -o "$P/lib/lua/5.4/lfs.so"
|
||||
gcc -O2 -fPIC -shared -I/usr/include/lua5.4 -Iluaexpat-1.5.2/src luaexpat-1.5.2/src/lxplib.c -lexpat -o "$P/lib/lua/5.4/lxp.so" 2>/dev/null
|
||||
cp luaexpat-1.5.2/src/lxp/*.lua "$P/share/lua/5.4/lxp/"
|
||||
(cd luasec-1.3.2 && make linux INC_PATH="-I/usr/include/lua5.4" LIB_PATH="" >/dev/null && make install LUAPATH="$P/share/lua/5.4" LUACPATH="$P/lib/lua/5.4" >/dev/null)
|
||||
(cd prosody-13.0.7 && ./configure --lua-version=5.4 --with-lua-include=/usr/include/lua5.4 --prefix="$P/prosody" --no-example-certs >/dev/null && make >/dev/null && make install >/dev/null)
|
||||
|
||||
cat > "$P/prosody.cfg.lua" <<CFG
|
||||
prosodyctl_service_warnings = false
|
||||
pidfile = "$P/prosody.pid"
|
||||
data_path = "$P/data"
|
||||
certificates = "$P/certs"
|
||||
log = { info = "$P/prosody.log"; error = "$P/prosody.err" }
|
||||
c2s_ports = { 5222 }
|
||||
http_ports = { 5280 }
|
||||
http_interfaces = { "127.0.0.1" }
|
||||
https_ports = {}
|
||||
interfaces = { "127.0.0.1" }
|
||||
c2s_require_encryption = false
|
||||
allow_unencrypted_plain_auth = true
|
||||
authentication = "internal_hashed"
|
||||
allow_registration = true
|
||||
modules_enabled = {
|
||||
"roster"; "saslauth"; "disco"; "carbons"; "pep"; "private"; "blocklist";
|
||||
"vcard4"; "vcard_legacy"; "version"; "uptime"; "time"; "ping"; "register";
|
||||
"mam"; "csi_simple"; "smacks"; "bookmarks"; "server_contact_info";
|
||||
}
|
||||
modules_disabled = { "s2s"; "tls" }
|
||||
archive_expires_after = "never"
|
||||
default_archive_policy = true
|
||||
http_host = "localhost"
|
||||
http_external_url = "http://localhost:5280/"
|
||||
VirtualHost "localhost"
|
||||
Component "conference.localhost" "muc"
|
||||
modules_enabled = { "muc_mam" }
|
||||
Component "upload.localhost" "http_file_share"
|
||||
http_file_share_size_limit = 20*1024*1024
|
||||
CFG
|
||||
|
||||
"$D/prosody.sh" start
|
||||
sleep 2
|
||||
"$D/prosody.sh" register alice localhost alicepass || true
|
||||
"$D/prosody.sh" register bob localhost bobpass || true
|
||||
"$D/prosody.sh" register carol localhost carolpass || true
|
||||
echo "Prosody is running on 127.0.0.1:5222"
|
||||
@@ -0,0 +1,17 @@
|
||||
import asyncio, json
|
||||
from e2e import Client
|
||||
|
||||
async def main():
|
||||
alice = Client("alice@localhost", "alicepass")
|
||||
bob = Client("bob@localhost", "bobpass")
|
||||
await alice.login(); await bob.login()
|
||||
await alice.call("Notifications", "ajaxAdd", [{"searchjid": {"value": "bob@localhost"}, "alias": {"value": "Bob"}, "group": {"value": "Friends"}}])
|
||||
await bob.recv_until(lambda m: m.get("func") == "Notifications.setCounters" and m["p"][0] not in ("", 0), timeout=10)
|
||||
print("bob got the request")
|
||||
await bob.call("Notifications", "ajaxAccept", ["alice@localhost"])
|
||||
await asyncio.sleep(3)
|
||||
await alice.call("Search", "ajaxHttpInitRoster")
|
||||
m = await alice.recv_until(lambda m: m.get("func") == "MovimTpl.fill" and m["p"][0] == "#roster")
|
||||
print("alice roster has bob:", "bob@localhost" in m["p"][1], "online:", "status online" in m["p"][1])
|
||||
|
||||
asyncio.run(main())
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Reference in New Issue
Block a user