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.
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 345 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -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()
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||