from playwright.sync_api import expect from django.urls import reverse from .base import BasePage class MessageReadsPage(BasePage): def acknowledgement(self, read_path, message): return self.page.expect_response(lambda response: response.request.method == 'POST' and response.url.endswith(read_path) and str(message.public_id) in (response.request.post_data_json or {}).get('message_ids', [])) def open_and_acknowledge(self, path, read_path, message): with self.acknowledgement(read_path, message): self.goto(path) expect(self.message(message)).to_be_in_viewport() def message(self, message): return self.page.locator(f'[data-message-id="{message.public_id}"]') def stop_initial_follow(self): self.page.locator('[data-conversation]').hover() self.page.mouse.wheel(0, -1) def scroll_and_acknowledge(self, message, read_path): self.stop_initial_follow() with self.acknowledgement(read_path, message): self.message(message).scroll_into_view_if_needed() expect(self.message(message)).to_be_in_viewport() def expect_unseen(self, message): expect(self.message(message).locator('.message-receipts')).to_have_count(0) def expect_seen_by(self, message, name): self.stop_initial_follow() self.message(message).locator('.message-receipts summary').click() expect(self.message(message).locator('.message-receipts')).to_contain_text(name) def emulate_background_tab(self): # Headless Chromium can keep multiple pages visible; control this signal # explicitly to test the same visibilitychange branch deterministically. self.page.add_init_script("Object.defineProperty(document, 'visibilityState', {get: () => window.receiptVisibility || 'hidden'});") def activate_and_acknowledge(self, message, read_path): with self.acknowledgement(read_path, message): self.page.evaluate("window.receiptVisibility = 'visible'; document.dispatchEvent(new Event('visibilitychange'));") def open_in_background(self, path, poll_path): with self.page.expect_response(lambda response: response.url.split('?')[0].endswith(poll_path)): self.goto(path) def fail_first_acknowledgement(self, read_path): def fail(route): route.fulfill(status=503, content_type='application/json', body='{}') self.page.route('**' + read_path, fail, times=1) def wait_for_successful_acknowledgement(self, read_path): with self.page.expect_response(lambda response: response.url.endswith(read_path) and response.status == 200): pass class MessageReadsRacePage(MessageReadsPage): def hold_poll_after_receiving_its_body(self): self.page.add_init_script(""" const nativeFetch = window.fetch.bind(window); window.fetch = async (input, options) => { const url = new URL(input, location.href); const polling = url.pathname.includes('/messages/poll/'); if (polling) { url.searchParams.delete('version'); url.searchParams.delete('cursor'); } const response = await nativeFetch(polling ? url : input, options); if (polling && response.ok && !window.blockedPoll) { window.blockedPoll = true; const json = response.json.bind(response); response.json = async () => { const data = await json(); await new Promise(resolve => { window.releaseHeldPoll = resolve; }); window.heldPollParsed = true; return data; }; } return response; }; """) def revoke_ticket(self, ticket, recipient, session_cookie, csrf): self.page.context.request.post(self.url(reverse('staff_tickets:revoke', args=[ticket.public_id, recipient.public_id])), headers={'Cookie': session_cookie, 'X-CSRFToken': csrf}, max_redirects=0) def scroll_to_revoked_message(self, message, read_path): self.stop_initial_follow() with self.page.expect_response(lambda response: response.url.endswith(read_path) and response.status == 403): self.message(message).scroll_into_view_if_needed() expect(self.page.get_by_text('دسترسی به این مکاتبات لغو شده است.', exact=True)).to_be_visible() def release_poll_and_expect_no_messages(self): self.page.wait_for_function('typeof window.releaseHeldPoll === "function"') self.page.evaluate('window.releaseHeldPoll()') self.page.wait_for_function('window.heldPollParsed === true') self.page.evaluate('() => new Promise(requestAnimationFrame)') expect(self.page.locator('[data-message-id]')).to_have_count(0) expect(self.page.get_by_text('دسترسی به این مکاتبات لغو شده است.', exact=True)).to_be_visible()