feat: Move logout button to menu list on mobile

- Added a new mobile-specific "Logout" button to the profile navigation list, visible only on screens smaller than `lg`.
- Hid the existing footer logout button on mobile screens.
- Updated `ProfileModalController` to bind the logout logic to the new mobile button.
- Applied `data-variant="critical"` and spacing to the new button to match styling requirements.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-20 14:26:54 +00:00
1 parent facad9dd80
commit 486c8af4a1
36 files changed
+24 -942

No files matched your search

+9 -1
View File
@@ -186,6 +186,14 @@
>
Admin (Moderation)
</button>
<button
id="profileMobileLogoutBtn"
type="button"
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium mt-6 lg:hidden"
data-variant="critical"
>
Logout
</button>
</nav>
</div>
@@ -1344,7 +1352,7 @@
</div>
<!-- Modal Footer -->
<div class="flex items-center justify-end gap-3 border-t border-border/60 p-4">
<div class="hidden lg:flex items-center justify-end gap-3 border-t border-border/60 p-4">
<button id="profileLogoutBtn" class="btn focus-ring" data-variant="danger">
Logout
</button>
+1 -1
View File
@@ -11,4 +11,4 @@
--player-modal-primary-inline-padding-lg,clamp(var(--space-sm),2.75vw,var(--space-lg))
)}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem;min-width:0;min-height:0}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem;min-width:0;min-height:0;width:100%;align-self:stretch}.video-modal__similar{display:none}@media (min-width:1024px){#playerModal.video-modal__similar{display:block}.video-modal__similar{display:flex;flex-direction:column;gap:1.5rem;align-self:stretch;flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module{height:100%;display:flex;flex:1 1 auto;min-height:0;flex-direction:column}#playerModal .video-modal__similar .player-modal__module-list{flex:1 1 auto;min-height:0}#playerModal .video-modal__similar .player-modal__module-scroll{height:100%;max-height:none;min-height:0}}#playerModal .player-modal__module-item{width:100%}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max))!important}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__media,#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__media{flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))!important}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.\!card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}#playerModal .video-modal__similar .player-modal__module-list{gap:clamp(var(--space-2xs),.85vw,var(--space-sm))}.player-modal__similar-card{outline:2px solid transparent;outline-offset:2px}.player-modal__similar-card:focus-visible{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:0.7;--tw-ring-offset-width:2px;--tw-ring-offset-color:var(--surface-base);transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-fast);transition-timing-function:cubic-bezier(0,0,.2,1);--tw-ring-color:var(--color-accent)}#playerModal.player-modal__similar-card{display:block}.player-modal__similar-card{position:relative;displayLine truncated
--motion-video-card-backdrop-translate
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.watch-history-card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.watch-history-card[data-state=disabled],.watch-history-card[data-state=private]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.watch-history-card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.watch-history-card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card img[data-thumbnail-state=blurred],.watch-history-card img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.watch-history-card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max));flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.watch-history-card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}#playerModal.watch-history-card{display:block}.watch-history-card{display:flex;flex-wrap:wrap;align-items:stretch;gap:1.25rem;padding:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}@media (prefers-reduced-motion:reduce){.watch-history-card,.watch-history-card[data-motion=enter]{transition-property:none}.watch-history-card[data-motion=enter]{opacity:1;animation:none}}.watch-history-card:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.watch-history-card{gap:1.5rem;padding:1.25rem}}.watch-history-card--private{border-color:var(--color-info);--tw-border-opacity:0.6}#playerModal.watch-histoLine truncated
);--video-card-backdrop-scale:1.12;--video-card-backdrop-blur:var(--blur-video-card-backdrop-strong);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card:focus-visible{border-color:var(--color-accent);--tw-border-opacity:0.4;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=critical]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}.watch-history-card[data-state=private]{background-color:var(--surface-raised-private);--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.watch-history-card[data-state=disabled],.watch-history-card[data-state=private]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card[data-state=disabled]{opacity:.6;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.watch-history-card[data-alert=nsfw-owner]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--color-black)}.watch-history-card[data-alert=nsfw-owner]:hover{--tw-border-opacity:0.6;--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.watch-history-card img[data-thumbnail-state=blurred],.watch-history-card img[data-visual-state=blurred]{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(.4,0,.2,1);filter:blur(var(--blur-thumbnail-blur));transform:scale(1.05)}.watch-history-card[data-owner-is-viewer=true][data-url-health-state=offline][data-stream-health-state=unhealthy]{border-width:1px;border-color:var(--color-critical);--tw-border-opacity:0.4;background-color:var(--surface-raised-critical)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card){display:flex;width:100%;flex-direction:row;flex-wrap:nowrap;align-items:stretch;gap:var(--space-md)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__media{width:clamp(var(--video-modal-item-media-min),18vw,var(--video-modal-item-media-max));flex-shrink:0;overflow:hidden;border-radius:var(--radius-lg)}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{padding:clamp(var(--space-sm),1.25vw,var(--space-md))}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__content{display:flex;min-width:0;flex:1 1 0%;flex-direction:column;gap:var(--space-sm)}#playerModal#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:block}#playerModal .player-modal__module-item>.watch-history-card:not(.player-modal__similar-card) .video-card__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:var(--font-size-xs);line-height:var(--line-height-tight);color:var(--text-muted-strong)}.watch-history-card[data-motion=enter]{opacity:0;animation:card-motion-enter .22s ease-out forwards}#playerModal.watch-history-card{display:block}.watch-history-card{display:flex;flex-wrap:wrap;align-items:stretch;gap:1.25rem;padding:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1)}@media (prefers-reduced-motion:reduce){.watch-history-card,.watch-history-card[data-motion=enter]{transition-property:none}.watch-history-card[data-motion=enter]{opacity:1;animation:none}}.watch-history-card:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@media (min-width:640px){.watch-history-card{gap:1.5rem;padding:1.25rem}}.watch-history-card--private{border-color:var(--color-info);--tw-border-opacity:0.6}#playerModal.watch-histoLine truncated
+14
View File
@@ -1211,6 +1211,7 @@ export class ProfileModalController {
this.globalProfileAvatar = null;
this.closeButton = null;
this.logoutButton = null;
this.mobileLogoutButton = null;
this.channelLink = null;
this.addAccountButton = null;
this.navButtons = {
@@ -1475,6 +1476,8 @@ export class ProfileModalController {
this.profileModalBackButton =
document.getElementById("profileModalBack") || null;
this.logoutButton = document.getElementById("profileLogoutBtn") || null;
this.mobileLogoutButton =
document.getElementById("profileMobileLogoutBtn") || null;
this.channelLink = document.getElementById("profileChannelLink") || null;
this.addAccountButton =
document.getElementById("profileAddAccountBtn") || null;
@@ -5028,6 +5031,17 @@ export class ProfileModalController {
});
}
if (this.mobileLogoutButton instanceof HTMLElement) {
this.mobileLogoutButton.addEventListener("click", async () => {
try {
await this.callbacks.onLogout(this);
} catch (error) {
this.showError("Failed to logout. Please try again.");
}
this.hide();
});
}
if (this.profileModalBackButton instanceof HTMLElement) {
this.profileModalBackButton.addEventListener("click", () => {
this.setMobileView("menu", { focusMenu: true });
-9
View File
@@ -1,9 +0,0 @@
npm error Missing script: "start"
npm error
npm error Did you mean one of these?
npm error npm star # Mark your favorite packages
npm error npm stars # View packages marked as favorites
npm error
npm error To see a list of scripts, run:
npm error npm run
npm error A complete log of this run can be found in: /home/jules/.npm/_logs/2026-01-19T20_34_33_214Z-debug-0.log
Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

-64
View File
@@ -1,64 +0,0 @@
from playwright.sync_api import sync_playwright, expect
def verify_avatar_size():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Load the index page directly via file protocol (assuming no build step is needed for CSS)
# Note: In a real environment we might need a server, but let's try direct file access first
# as tailwind.generated.css might be needed.
# Checking if we need to build first.
# Actually, let's just inspect the CSS rules applied to an element we create dynamically
# or load a simple HTML that imports the CSS.
# Let's try to load the app. If it fails, we will create a minimal reproduction.
try:
# We need to serve the files. Let's use python's http.server in background?
# Or just assume we can open index.html if it exists.
import os
cwd = os.getcwd()
page.goto(f"file://{cwd}/index.html")
# The app might need JS to render anything.
# Let's inject a contact row manually into the body to test the CSS.
page.evaluate("""
document.body.innerHTML = `
<div class="dm-contact-row" style="width: 200px; display: flex;">
<div class="dm-avatar dm-avatar--md" style="background: red;">
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>" />
</div>
<div class="dm-contact-row__content" style="flex: 1; min-width: 0;">
<div class="dm-contact-row__name">A very long name that should normally shrink the avatar if flex-shrink is not set to 0</div>
</div>
</div>
`;
""")
# Allow some time for styles to apply if they are loaded async
page.wait_for_timeout(1000)
# Take a screenshot
page.screenshot(path="verification/avatar_verification.png")
# Also evaluate the computed style of the avatar
flex_shrink = page.evaluate("""
window.getComputedStyle(document.querySelector('.dm-avatar')).flexShrink
""")
print(f"Computed flex-shrink: {flex_shrink}")
if flex_shrink == '0':
print("SUCCESS: flex-shrink is 0")
else:
print("FAILURE: flex-shrink is NOT 0")
except Exception as e:
print(f"Error: {e}")
finally:
browser.close()
if __name__ == "__main__":
verify_avatar_size()
-30
View File
@@ -1,30 +0,0 @@
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// Navigate to the index page locally
await page.goto('file://' + process.cwd() + '/index.html');
// Set viewport to simulate mobile for some checks
await page.setViewportSize({ width: 375, height: 667 });
// Wait for Tailwind classes to be effective (loaded from CSS)
// We can't easily wait for CSS file load in file protocol without network idle,
// but file protocol is fast.
await page.waitForTimeout(1000);
// Take screenshot of mobile view (to verify search bar width fix)
await page.screenshot({ path: 'verification/mobile-view.png' });
// Navigate to profile modal by finding a trigger (this is harder without a full app running)
// But we can check if the profile modal component file has the right classes by reading it?
// No, visual verification of static HTML file is better.
// Let's just screenshot index.html in mobile view which had the w-[calc...] fix.
await browser.close();
})();
-31
View File
@@ -1,31 +0,0 @@
import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// Navigate to the index page locally
await page.goto('file://' + process.cwd() + '/index.html');
// Set viewport to simulate mobile for some checks
await page.setViewportSize({ width: 375, height: 667 });
// Wait for Tailwind classes to be effective
await page.waitForTimeout(1000);
// Take screenshot of mobile view (to verify search bar width fix)
// The search bar is hidden by default, we might need to reveal it or just check layout.
// The class was: fixed bottom-6 right-6 z-50 hidden w-[calc(100%-var(--sidebar-mobile-gutter))]
// We can try to make it visible.
await page.evaluate(() => {
const el = document.getElementById('mobileSearchContainer');
if (el) el.classList.remove('hidden');
});
await page.screenshot({ path: 'verification/mobile-view.png' });
await browser.close();
})();
-68
View File
@@ -1,68 +0,0 @@
from playwright.sync_api import sync_playwright
def verify_messages_dot():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# Set a viewport that triggers the mobile menu view (or desktop, depending on where the nav is)
# Profile modal uses a split layout on desktop (lg), and mobile view on smaller screens.
# The prompt implies the "Messages" tab button which is visible in the sidebar navigation
# inside the modal.
# Let's target a desktop size to see the sidebar layout clearly.
page = browser.new_page(viewport={"width": 1280, "height": 800})
# Navigate to the app (assuming it's served on port 8000 based on memory)
page.goto("http://localhost:8000/index.html")
# Wait for page load
page.wait_for_load_state("networkidle")
# The profile modal is hidden by default. We need to open it.
# Usually there is a profile button or we can force it open via JS.
# Looking at index.html, there is a #profileButton (hidden by default) and #loginButton.
# Let's try to simulate a logged-in state or just force the modal open.
# Force the profile modal to be visible and have the 'Messages' tab unread dot visible.
page.evaluate("""
const modal = document.getElementById('profileModal');
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
// Force the unread dot to be visible
const dot = document.getElementById('profileMessagesUnreadDot');
if (dot) {
dot.classList.add('is-visible');
dot.style.display = 'block'; // Ensure it overrides any specific CSS if needed for test
}
// Highlight the messages button to simulate active state if needed,
// but the prompt asked for visibility even when selected (red).
// Let's make the messages button active.
const msgBtn = document.getElementById('profileNavMessages');
if (msgBtn) {
msgBtn.setAttribute('data-state', 'active');
msgBtn.setAttribute('aria-selected', 'true');
}
""")
# Wait a moment for animations/styles
page.wait_for_timeout(1000)
# Take a screenshot of the navigation area in the profile modal
# We target the profile modal nav or the specific button area.
nav = page.locator("#profileNavMessages")
if nav.is_visible():
# Capture the button and some surrounding area to see the dot overhang
# We can take a screenshot of the whole modal nav sidebar
sidebar = page.locator(".profile-modal__nav")
sidebar.screenshot(path="verification/messages_dot_verification.png")
print("Screenshot taken: verification/messages_dot_verification.png")
else:
print("Profile nav messages button not visible.")
# Fallback full page
page.screenshot(path="verification/full_page_fallback.png")
browser.close()
if __name__ == "__main__":
verify_messages_dot()
-95
View File
@@ -1,95 +0,0 @@
from playwright.sync_api import sync_playwright
import time
def verify_moderation(page):
print("Navigating to app...")
page.goto("http://localhost:8080")
# Wait for the app to initialize
page.wait_for_selector("#videoList")
# Inject script to open a mock moderated video
print("Injecting mock video...")
page.evaluate("""
const mockVideo = {
id: "mock-video-1",
title: "Moderated Video Test",
description: "This is a test video.",
pubkey: "mock-pubkey",
moderation: {
trustedMuted: true,
shouldShow: true,
trustedMuteCount: 5,
trustedMuteDisplayNames: ["Alice", "Bob"]
}
};
// Access the app instance from the window object (assuming it's attached or reachable)
// If not attached, we might need to find where 'app' variable is stored.
// Looking at app.js, it seems 'app' is not globally exposed by default,
// but often it is assigned to window.app in main.js or index.js.
// Let's try to find the videoModal instance via DOM if app is not global
// But app.js likely attaches it to window for debugging or it's in a module.
// Assumption: window.app is available. If not, we might fail here.
if (window.app && window.app.videoModal) {
window.app.videoModal.open(mockVideo);
} else {
console.error("window.app.videoModal not found");
}
""")
# Wait for modal to open
page.wait_for_selector("#playerModal:not(.hidden)")
print("Modal opened.")
# Take screenshot of BLOCKED state
# We expect the moderation bar to be visible above the video
time.sleep(1) # Wait for animations
page.screenshot(path="verification/1_blocked_state.png")
print("Screenshot 1 taken: Blocked state")
# Check if moderation bar is present
bar = page.locator("[data-moderation-bar]")
if bar.is_visible():
print("Moderation bar is visible.")
else:
print("ERROR: Moderation bar is NOT visible.")
# Click "Show anyway"
print("Clicking 'Show anyway'...")
page.click("[data-moderation-action='override']")
# Wait for update
time.sleep(1)
# Take screenshot of UNBLOCKED state
page.screenshot(path="verification/2_unblocked_state.png")
print("Screenshot 2 taken: Unblocked state")
# Check if moderation bar is STILL visible
if bar.is_visible():
print("Moderation bar is visible (Correct).")
# Check text content
text = page.locator("[data-moderation-text]").inner_text()
print(f"Moderation text: {text}")
if "Content or user blocked" in text: # It might still say blocked, but button should change
print("Text verification needs update based on logic.")
# Check if button changed to "Restore default moderation" (or "Hide")
button_text = page.locator("[data-moderation-action='hide']").inner_text()
print(f"Button text: {button_text}")
else:
print("ERROR: Moderation bar is NOT visible (Incorrect).")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
verify_moderation(page)
except Exception as e:
print(f"Verification failed: {e}")
finally:
browser.close()
-91
View File
@@ -1,91 +0,0 @@
from playwright.sync_api import sync_playwright
import time
def verify_moderation_dom(page):
print("Navigating to app...")
page.goto("http://localhost:8080")
# Wait for the app to initialize
print("Waiting for #videoList...")
page.wait_for_selector("#videoList", timeout=30000)
# Inject logic to manipulate DOM directly to verify structure
# This bypasses the need for the app logic to work perfectly in this headless environment
# We just want to ensure the HTML structure and CSS are correct.
print("Manipulating DOM to show modal...")
page.evaluate("""
const modal = document.getElementById('playerModal');
modal.classList.remove('hidden');
modal.removeAttribute('hidden');
document.body.classList.add('modal-open');
// Find the moderation bar and show it
const bar = modal.querySelector('[data-moderation-bar]');
if (bar) {
bar.removeAttribute('hidden');
// Force text
const text = bar.querySelector('[data-moderation-text]');
if (text) text.textContent = "BLOCKED TEST";
}
// Blur the video container
const stage = modal.querySelector('.video-modal__video');
if (stage) {
stage.dataset.visualState = 'blurred';
}
""")
time.sleep(1)
page.screenshot(path="verification/1_forced_blocked.png")
print("Screenshot 1 taken: Forced Blocked state")
# Verify structure: Bar should be strictly before Video container
# .video-modal__moderation-bar should be previous sibling of .video-modal__video (ignoring text nodes/comments if any, but in flex column gap-6)
# Actually, they are siblings in .video-modal__stage (or primary)
# Let's check their order via bounding box
bar_bbox = page.locator("[data-moderation-bar]").bounding_box()
video_bbox = page.locator(".video-modal__video").bounding_box()
if bar_bbox and video_bbox:
print(f"Bar Y: {bar_bbox['y']}, Video Y: {video_bbox['y']}")
if bar_bbox['y'] < video_bbox['y']:
print("SUCCESS: Moderation bar is above the video.")
else:
print("FAILURE: Moderation bar is NOT above the video.")
else:
print("FAILURE: Could not find bounding boxes.")
# Now simulate "Show Anyway" -> Remove blur but KEEP bar
print("Simulating 'Show Anyway'...")
page.evaluate("""
const modal = document.getElementById('playerModal');
// Unblur
const stage = modal.querySelector('.video-modal__video');
if (stage) {
delete stage.dataset.visualState;
}
// Bar stays visible!
// (No action needed as it is already visible, but we verify it stays)
""")
time.sleep(1)
page.screenshot(path="verification/2_forced_shown.png")
print("Screenshot 2 taken: Forced Shown state")
bar_visible = page.locator("[data-moderation-bar]").is_visible()
if bar_visible:
print("SUCCESS: Moderation bar remains visible after unblurring.")
else:
print("FAILURE: Moderation bar disappeared.")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
verify_moderation_dom(page)
except Exception as e:
print(f"Verification failed: {e}")
finally:
browser.close()
-123
View File
@@ -1,123 +0,0 @@
from playwright.sync_api import sync_playwright
import time
def verify_moderation_style(page):
print("Navigating to app...")
page.goto("http://localhost:8080")
# Wait for the app to initialize
print("Waiting for #videoList...")
page.wait_for_selector("#videoList", timeout=30000)
# Inject logic to manipulate DOM directly to verify structure and styles
print("Manipulating DOM to show modal...")
page.evaluate("""
const modal = document.getElementById('playerModal');
modal.classList.remove('hidden');
modal.removeAttribute('hidden');
document.body.classList.add('modal-open');
// Find the moderation bar and show it in "neutral" state (simulating "Show anyway")
const bar = modal.querySelector('[data-moderation-bar]');
if (bar) {
bar.removeAttribute('hidden');
const badge = bar.querySelector('.moderation-badge');
if (badge) {
badge.setAttribute('data-variant', 'neutral');
const text = badge.querySelector('[data-moderation-text]');
if (text) text.textContent = "Showing despite blocked content";
}
}
// Unblur video but blur avatar to simulate "active moderation" state visually
// (Though logically if "neutral" usually implies override, maybe we want to test that too.
// Let's test the specific requirement: Avatar Blur + Text Color)
// Force blur on avatar
const avatarImg = modal.querySelector('.channel-card__avatar img');
if (avatarImg) {
avatarImg.src = 'assets/svg/default-profile.svg'; // Ensure src so it renders
avatarImg.dataset.visualState = 'blurred';
}
""")
time.sleep(1)
# Test Dark Mode first (usually default or we can toggle)
# Check if we can toggle theme.
# Assuming default might be light or system. Let's force dark mode if possible.
print("Setting Dark Mode...")
page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')")
time.sleep(0.5)
page.screenshot(path="verification/3_dark_mode_neutral.png")
print("Screenshot 3 taken: Dark Mode Neutral Badge + Blurred Avatar")
# Verify Avatar Blur CSS
# We can check computed style via evaluate
blur_filter = page.evaluate("""
() => {
const img = document.querySelector('#playerModal .channel-card__avatar img');
return window.getComputedStyle(img).filter;
}
""")
print(f"Avatar Filter: {blur_filter}")
if "blur" in blur_filter:
print("SUCCESS: Avatar has blur filter applied.")
else:
print("FAILURE: Avatar does NOT have blur filter.")
# Verify Text Color in Dark Mode
# The badge text should be readable (light color in dark mode)
text_color = page.evaluate("""
() => {
const badge = document.querySelector('#playerModal .video-modal__moderation-bar .moderation-badge');
return window.getComputedStyle(badge).color;
}
""")
print(f"Badge Text Color (Dark Mode): {text_color}")
# In dark mode, text should be rgb(226, 232, 240) or similar light color.
# rgb(24, 31, 44) is dark.
# Simple check: sum of RGB should be high for light color
import re
rgb = re.findall(r'\d+', text_color)
if rgb and len(rgb) >= 3:
brightness = sum(map(int, rgb[:3]))
print(f"Text Brightness: {brightness}")
if brightness > 300: # Arbitrary threshold for "light enough"
print("SUCCESS: Text is light in Dark Mode.")
else:
print("FAILURE: Text is dark in Dark Mode (Hard to read).")
# Test Light Mode
print("Setting Light Mode...")
page.evaluate("document.documentElement.setAttribute('data-theme', 'light')")
time.sleep(0.5)
page.screenshot(path="verification/4_light_mode_neutral.png")
print("Screenshot 4 taken: Light Mode Neutral Badge")
text_color_light = page.evaluate("""
() => {
const badge = document.querySelector('#playerModal .video-modal__moderation-bar .moderation-badge');
return window.getComputedStyle(badge).color;
}
""")
print(f"Badge Text Color (Light Mode): {text_color_light}")
rgb_light = re.findall(r'\d+', text_color_light)
if rgb_light and len(rgb_light) >= 3:
brightness_light = sum(map(int, rgb_light[:3]))
print(f"Text Brightness: {brightness_light}")
if brightness_light < 400: # Arbitrary threshold for "dark enough"
print("SUCCESS: Text is dark in Light Mode.")
else:
print("FAILURE: Text is light in Light Mode (Hard to read).")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
verify_moderation_style(page)
except Exception as e:
print(f"Verification failed: {e}")
finally:
browser.close()
-51
View File
@@ -1,51 +0,0 @@
from playwright.sync_api import sync_playwright
def verify_profile_modal(page):
page.goto("http://localhost:8001")
# 1. Open the profile modal by clicking the avatar button
# Assuming standard header layout; if not found, we will adapt.
# The header avatar usually has an ID like 'profileAvatar' or 'headerProfileButton'.
# I'll try to find a button with an image or "profile" in the name/aria-label.
# The button ID is profileButton
page.locator("#profileButton").click()
# 2. Wait for modal to appear
# The modal ID is #profileModal
page.locator("#profileModal").wait_for(state="visible")
# 3. Navigate to Messages tab
# Button ID: #profileNavMessages
page.locator("#profileNavMessages").click()
# 4. Wait for messages pane to be visible
page.locator("#profilePaneMessages").wait_for(state="visible")
# 5. Take a screenshot of the full viewport to verify modal size and layout
page.screenshot(path="verification/profile_modal_full.png", full_page=True)
# 6. Take a screenshot specifically of the Composer area to check the "More" button
composer = page.locator(".dm-composer")
if composer.is_visible():
composer.screenshot(path="verification/composer_layout.png")
# 7. Click the "More" button to show the menu
more_btn = page.locator(".dm-composer__more")
more_btn.click()
page.wait_for_timeout(200) # Wait for menu to unhide
composer.screenshot(path="verification/composer_menu_open.png")
if __name__ == "__main__":
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# Use a desktop viewport size to verify full-screen behavior
page = browser.new_page(viewport={"width": 1280, "height": 800})
try:
verify_profile_modal(page)
except Exception as e:
print(f"Error: {e}")
# Take a debug screenshot if things fail
page.screenshot(path="verification/debug_fail.png")
finally:
browser.close()
-32
View File
@@ -1,32 +0,0 @@
from playwright.sync_api import sync_playwright
import os
def run():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={'width': 1920, 'height': 1080})
# Navigate to the local server
page.goto("http://localhost:8080/index.html")
# Wait for the app to be ready (look for the header logo or similar)
page.wait_for_selector(".ds-header__logo")
# Force the profile modal to be visible by adding 'data-state="open"' or removing 'hidden' class
# In the real app, we would click the profile button, but here we can just manipulate the DOM for testing the CSS
page.evaluate("""
const modal = document.getElementById('profileModal');
modal.classList.remove('hidden');
modal.style.display = 'flex'; // Ensure it's displayed as flex (from .bv-modal)
""")
# Wait a bit for transitions if any
page.wait_for_timeout(500)
# Take a screenshot of the full viewport to verify it covers everything
page.screenshot(path="verification/profile_modal_fullscreen.png")
browser.close()
if __name__ == "__main__":
run()
-38
View File
@@ -1,38 +0,0 @@
from playwright.sync_api import sync_playwright
def verify_mobile_search_fab():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# Emulate a mobile device to ensure the FAB is visible
# iPhone 12 Pro dimensions: 390x844
context = browser.new_context(viewport={'width': 390, 'height': 844})
page = context.new_page()
try:
# Navigate to the local server
page.goto("http://localhost:8000/index.html")
# Locate the mobile search FAB
fab = page.locator("#mobileSearchFab")
# Wait for it to be visible (it has a hidden class removed by JS logic or media query)
# The CSS shows md:hidden. We are on mobile viewport, so it should be visible.
# However, JS logic might also toggle it.
# Looking at index.js: "mobileSearchFab.classList.remove('hidden')" logic exists.
# But let's check its visibility.
# Check the computed style background color
bg_color = fab.evaluate("element => getComputedStyle(element).backgroundColor")
print(f"Computed background color: {bg_color}")
# Take a screenshot of the FAB
fab.screenshot(path="verification/mobile_search_fab.png")
print("Screenshot saved to verification/mobile_search_fab.png")
except Exception as e:
print(f"Error: {e}")
finally:
browser.close()
if __name__ == "__main__":
verify_mobile_search_fab()
-45
View File
@@ -1,45 +0,0 @@
from playwright.sync_api import sync_playwright
def run():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Load the HTML template file directly by fetching it and injecting it
# effectively unit-testing the template structure in a browser
page.goto("http://localhost:8080")
# Inject the modal HTML manually to verify the static changes
page.evaluate("""async () => {
const response = await fetch("/components/upload-modal.html");
const html = await response.text();
document.body.innerHTML = html;
document.getElementById("uploadModal").classList.remove("hidden");
document.getElementById("section-storage-settings").classList.remove("hidden");
}""")
# Wait for rendering
page.wait_for_selector("#uploadModal", state="visible")
# Check text
button_text = page.inner_text("#btn-storage-settings")
print(f"Button Text: {button_text}")
content = page.content()
if "Storage Configuration" in button_text:
print("Verified: Button text updated.")
if "Account ID / Endpoint" in content:
print("Verified: Account label updated.")
if "Public URL" in content:
print("Verified: Public URL label updated.")
page.screenshot(path="verification/upload_modal_static.png")
print("Screenshot saved to verification/upload_modal_static.png")
browser.close()
if __name__ == "__main__":
run()
-59
View File
@@ -1,59 +0,0 @@
from playwright.sync_api import sync_playwright, expect
def run_verification():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# Emulate a mobile device
context = browser.new_context(
viewport={"width": 375, "height": 667},
user_agent="Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
)
page = context.new_page()
# Navigate to local server
page.goto("http://localhost:8080/index.html")
# We need to mock a watch history card because the default state is empty
# and we don't want to rely on real network calls or complex state setup.
# We inject HTML directly into the body for verification of the CSS class.
mock_html = """
<div class="watch-history-card">
<div class="watch-history-card__primary flex flex-col">
<a class="watch-history-card__thumbnail block flex-none text-text no-underline w-56 max-w-full" href="#">
<div class="watch-history-card__thumbnailInner aspect-video w-full overflow-hidden rounded-lg bg-surface">
<img src="https://via.placeholder.com/640x360" style="width:100%; height:100%; object-fit:cover;">
</div>
</a>
<div class="watch-history-card__details">
<div class="watch-history-card__title">Test Video Title That Might Be Long</div>
</div>
</div>
</div>
"""
page.evaluate(f"document.body.innerHTML = `{mock_html}`")
# Wait for potential layout shifts
page.wait_for_timeout(500)
# Verify the computed style width of the thumbnail container
thumbnail = page.locator(".watch-history-card__thumbnail")
box = thumbnail.bounding_box()
print(f"Thumbnail width: {box['width']}px")
# On mobile, html font-size is 80% (12.8px). w-56 is 14rem.
# 14 * 12.8 = 179.2px.
# If w-full was active, it would be close to 375px (minus padding if parent had it, but here parent is div block, so likely full width).
# We check it's close to 179.2
assert abs(box['width'] - 179.2) < 2, f"Expected width ~179.2px, got {box['width']}px"
# Take screenshot
page.screenshot(path="verification/mobile_thumbnail_verification.png")
print("Screenshot saved to verification/mobile_thumbnail_verification.png")
browser.close()
if __name__ == "__main__":
run_verification()
-54
View File
@@ -1,54 +0,0 @@
from playwright.sync_api import sync_playwright
import os
def run():
# Construct absolute path to the local file
file_path = os.path.abspath('index.html')
url = f'file://{file_path}'
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
# Navigate to the local index.html
print(f'Navigating to {url}')
page.goto(url)
# Wait for the upload modal to be part of the DOM (it's in the static HTML but hidden)
# We need to trigger it to open.
# The upload button has id='uploadButton'.
# However, checking ApplicationBootstrap logic, it might need JS initialization.
# Since we are running against static file://, module scripts might fail CORS or path resolution
# unless served via http.
# Let's try to serve it first.
# Actually, simpler: just check if the new elements exist in the DOM,
# even if the modal isn't visible, we can force it visible via JS for the screenshot.
# Wait for the modal container
page.wait_for_selector('#uploadModal', state='attached')
# Force modal visible for screenshot
page.evaluate("document.getElementById('uploadModal').classList.remove('hidden')")
# Switch to Cloudflare tab to see new UI
# The tab buttons have data-upload-mode='cloudflare'
# We can click it or just unhide the section manually if JS isn't running fully.
# Let's assume JS might fail on file://, so we do manual DOM manipulation to reveal the section.
# Hide custom section
page.evaluate("document.getElementById('customUploadSection').classList.add('hidden')")
# Show Cloudflare section
page.evaluate("document.getElementById('cloudflareUploadSection').classList.remove('hidden')")
# Take screenshot of the Cloudflare section
element = page.locator('#cloudflareUploadSection')
element.screenshot(path='verification/upload_modal_cloudflare.png')
browser.close()
if __name__ == '__main__':
run()
-69
View File
@@ -1,69 +0,0 @@
from playwright.sync_api import sync_playwright, expect
def run(playwright):
browser = playwright.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to the app
page.goto("http://localhost:8080")
# Wait for app
page.wait_for_selector("#app")
# Dismiss disclaimer if present
try:
if page.locator("#disclaimerModal").is_visible():
page.locator("#disclaimerModal").get_by_role("button", name="I understand").click(timeout=2000)
except:
pass
# 1. Verify the "Upload Content" Page
print("Navigating to Upload Content view...")
page.goto("http://localhost:8080/#view=upload-content")
page.wait_for_timeout(2000)
expect(page.locator("h2#uploadContentHeading")).to_have_text("Upload Content")
page_icon = page.locator("#viewContainer img[src='assets/svg/upload-content-icon.svg']")
expect(page_icon).to_be_visible()
page.screenshot(path="verification/upload_content_page.png")
print("Screenshot of page saved.")
# 2. Verify the Link in Upload Modal
# Force remove 'hidden' class from uploadButton
page.evaluate("document.getElementById('uploadButton').classList.remove('hidden')")
# Wait for it to be visible? Playwright waits for visibility.
# Sometimes removing class isn't enough if parent is hidden, but parent is visible header.
# The previous error said "element is not visible".
# Let's force click via JS if UI click fails, or force visible style.
page.evaluate("document.getElementById('uploadButton').style.display = 'block'")
page.wait_for_selector("#uploadButton", state="visible")
page.locator("#uploadButton").click()
# Verify modal visible
modal = page.locator("#uploadModal")
# Modal also has 'hidden' class toggled by JS.
# Wait for it.
expect(modal).to_be_visible()
# Open storage settings
page.locator("#btn-storage-settings").click()
# Verify link
link = page.locator("#section-storage-settings").get_by_role("link", name="Learn How")
expect(link).to_be_visible()
expect(link).to_have_attribute("href", "#view=upload-content")
# Verify link icon
link_icon = link.locator("img")
expect(link_icon).to_have_attribute("src", "assets/svg/upload-content-icon.svg")
page.screenshot(path="verification/upload_modal_link.png")
print("Screenshot of modal saved.")
browser.close()
with sync_playwright() as playwright:
run(playwright)
-81
View File
@@ -1,81 +0,0 @@
from playwright.sync_api import sync_playwright, expect
import re
def run(playwright):
browser = playwright.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to the app
page.goto("http://localhost:8080")
# Wait for app
page.wait_for_selector("#app")
# Dismiss disclaimer if present
try:
if page.locator("#disclaimerModal").is_visible():
page.locator("#disclaimerModal").get_by_role("button", name="I understand").click(timeout=2000)
except:
pass
# 1. Verify the Sidebar Link
print("Verifying Sidebar Link...")
# Open "More" dropdown in sidebar
page.locator("#footerDropdownButton").click()
# Wait for dropdown
page.wait_for_selector("#footerLinksContainer:not(.hidden)")
# Check for "Upload Content" link
sidebar_link = page.get_by_role("link", name="Upload Content")
expect(sidebar_link).to_be_visible()
# Verify icon in sidebar link
sidebar_icon = sidebar_link.locator("img")
expect(sidebar_icon).to_have_attribute("src", "assets/svg/upload-content-icon.svg")
page.screenshot(path="verification/sidebar_link.png")
print("Screenshot of sidebar link saved.")
# 2. Verify the Modal Link Update
print("Verifying Modal Link...")
# Open modal
page.evaluate("document.getElementById('uploadButton').classList.remove('hidden')")
page.evaluate("document.getElementById('uploadButton').style.display = 'block'")
page.wait_for_selector("#uploadButton", state="visible")
page.locator("#uploadButton").click()
# Wait for modal
modal = page.locator("#uploadModal")
expect(modal).to_be_visible()
# Open storage settings
page.locator("#btn-storage-settings").click()
# Verify link is now button-like and no target=_blank
settings_section = page.locator("#section-storage-settings")
link = settings_section.get_by_role("link", name="Learn How")
expect(link).to_be_visible()
# Check class for button styling (using regex on the class attribute string directly)
class_attr = link.get_attribute("class")
if not re.search(r"btn.*btn-ghost.*btn-xs", class_attr):
raise Exception(f"Link classes do not match expected button styling. Found: {class_attr}")
# Check href
expect(link).to_have_attribute("href", "#view=upload-content")
# Check NO target="_blank"
target = link.get_attribute("target")
if target == "_blank":
raise Exception("Link still has target='_blank'")
print("Link target verified (not _blank).")
page.screenshot(path="verification/upload_modal_updated.png")
print("Screenshot of updated modal saved.")
browser.close()
with sync_playwright() as playwright:
run(playwright)