fix: improve Codex account settings
This commit is contained in:
Vendored
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
<script type="module" crossorigin src="/assets/index-BbkhVAJw.js"></script>
|
<script type="module" crossorigin src="/assets/index-Ddm_ZeSC.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BTO6fDiH.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B0FW1DHS.css">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+83
-43
@@ -52,6 +52,11 @@ type Account = {
|
|||||||
possibleDuplicate: boolean;
|
possibleDuplicate: boolean;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
};
|
};
|
||||||
|
type DeviceLogin = {
|
||||||
|
accountId: number;
|
||||||
|
verificationUrl: string;
|
||||||
|
userCode: string;
|
||||||
|
};
|
||||||
type Dash = {
|
type Dash = {
|
||||||
accountId: number;
|
accountId: number;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
@@ -685,7 +690,7 @@ function General() {
|
|||||||
}
|
}
|
||||||
function CodexSettings() {
|
function CodexSettings() {
|
||||||
const [xs, setXs] = useState<Account[]>([]),
|
const [xs, setXs] = useState<Account[]>([]),
|
||||||
[result, setResult] = useState<any>(null),
|
[deviceLogin, setDeviceLogin] = useState<DeviceLogin | null>(null),
|
||||||
[active, setActive] = useState(0),
|
[active, setActive] = useState(0),
|
||||||
[newKind, setNewKind] = useState<"personal" | "team">("team"),
|
[newKind, setNewKind] = useState<"personal" | "team">("team"),
|
||||||
[busy, setBusy] = useState(false),
|
[busy, setBusy] = useState(false),
|
||||||
@@ -695,13 +700,17 @@ function CodexSettings() {
|
|||||||
load();
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!result || !active) return;
|
if (!deviceLogin) return;
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const timer = setInterval(async () => {
|
const timer = setInterval(async () => {
|
||||||
const accounts = await load();
|
const accounts = await load();
|
||||||
const account = accounts.find((x) => x.id === active);
|
const account = accounts.find((x) => x.id === deviceLogin.accountId);
|
||||||
|
if (!account) {
|
||||||
|
setDeviceLogin(null);
|
||||||
|
clearInterval(timer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
account &&
|
|
||||||
["matched", "mismatch"].includes(account.validationStatus)
|
["matched", "mismatch"].includes(account.validationStatus)
|
||||||
)
|
)
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
@@ -709,13 +718,17 @@ function CodexSettings() {
|
|||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [result, active]);
|
}, [deviceLogin]);
|
||||||
const login = async (id: number) => {
|
const login = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setActive(id);
|
setActive(id);
|
||||||
setErr("");
|
setErr("");
|
||||||
setResult(await post(`accounts/${id}/login/device`));
|
setDeviceLogin(null);
|
||||||
|
const result = await post<Omit<DeviceLogin, "accountId">>(
|
||||||
|
`accounts/${id}/login/device`,
|
||||||
|
);
|
||||||
|
setDeviceLogin({ accountId: id, ...result });
|
||||||
} catch (q) {
|
} catch (q) {
|
||||||
setErr((q as Error).message);
|
setErr((q as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -727,13 +740,17 @@ function CodexSettings() {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setActive(0);
|
setActive(0);
|
||||||
setErr("");
|
setErr("");
|
||||||
|
setDeviceLogin(null);
|
||||||
const x = await post<Account>("accounts", {
|
const x = await post<Account>("accounts", {
|
||||||
displayName: `账号 ${xs.length + 1}`,
|
displayName: `账号 ${xs.length + 1}`,
|
||||||
expectedKind: newKind,
|
expectedKind: newKind,
|
||||||
});
|
});
|
||||||
await load();
|
await load();
|
||||||
setActive(x.id);
|
setActive(x.id);
|
||||||
setResult(await post(`accounts/${x.id}/login/device`));
|
const result = await post<Omit<DeviceLogin, "accountId">>(
|
||||||
|
`accounts/${x.id}/login/device`,
|
||||||
|
);
|
||||||
|
setDeviceLogin({ accountId: x.id, ...result });
|
||||||
} catch (q) {
|
} catch (q) {
|
||||||
setErr((q as Error).message);
|
setErr((q as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -741,16 +758,19 @@ function CodexSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="panel form wide">
|
<div className="codex-settings wide">
|
||||||
<div className="settings-title">
|
<div className="codex-heading">
|
||||||
<div>
|
|
||||||
<h2>Codex 账户与工作区</h2>
|
<h2>Codex 账户与工作区</h2>
|
||||||
<p>
|
<p>个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。</p>
|
||||||
个人订阅和 Team 工作区请分别添加为独立连接;同一邮箱可以添加多次。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<section className="panel add-account-card">
|
||||||
|
<div>
|
||||||
|
<h3>添加新连接</h3>
|
||||||
|
<p>选择要连接的订阅类型,然后使用设备码完成授权。</p>
|
||||||
|
</div>
|
||||||
|
<div className="add-account-controls">
|
||||||
<label>
|
<label>
|
||||||
新连接类型
|
连接类型
|
||||||
<select
|
<select
|
||||||
value={newKind}
|
value={newKind}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -765,11 +785,18 @@ function CodexSettings() {
|
|||||||
{busy && active === 0 ? "服务启动中…" : "添加账号"}
|
{busy && active === 0 ? "服务启动中…" : "添加账号"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<div className="account-list">
|
<div className="account-list">
|
||||||
|
{xs.length === 0 && (
|
||||||
|
<div className="panel empty account-empty">
|
||||||
|
尚未添加 Codex 账号,请使用上方的“添加账号”创建连接。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{xs.map((x) => (
|
{xs.map((x) => (
|
||||||
<div className="account-row" key={x.id}>
|
<section className="panel account-card" key={x.id}>
|
||||||
|
<div className="account-card-main">
|
||||||
<span className={"dot " + (x.connected ? "ok" : "")} />
|
<span className={"dot " + (x.connected ? "ok" : "")} />
|
||||||
<div>
|
<div className="account-details">
|
||||||
<input
|
<input
|
||||||
aria-label="连接名称"
|
aria-label="连接名称"
|
||||||
defaultValue={x.displayName}
|
defaultValue={x.displayName}
|
||||||
@@ -784,18 +811,23 @@ function CodexSettings() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<small>
|
<div className="account-meta">
|
||||||
{x.email || "尚未登录"} · {planLabel(x.planType)}
|
<span>{x.email || "尚未登录"}</span>
|
||||||
</small>
|
<span>{planLabel(x.planType)}</span>
|
||||||
<small className={x.validationStatus === "mismatch" ? "error" : ""}>
|
<span className={x.validationStatus === "mismatch" ? "error" : ""}>
|
||||||
{validationLabel(x)}
|
{validationLabel(x)}
|
||||||
</small>
|
</span>
|
||||||
|
</div>
|
||||||
{x.possibleDuplicate && (
|
{x.possibleDuplicate && (
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
同一邮箱已有相同类型连接,请确认没有重复授权同一工作区
|
同一邮箱已有相同类型连接,请确认没有重复授权同一工作区
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="account-card-actions">
|
||||||
|
<label>
|
||||||
|
预期连接类型
|
||||||
<select
|
<select
|
||||||
aria-label="预期连接类型"
|
aria-label="预期连接类型"
|
||||||
value={x.expectedKind}
|
value={x.expectedKind}
|
||||||
@@ -811,21 +843,24 @@ function CodexSettings() {
|
|||||||
<option value="personal">个人订阅</option>
|
<option value="personal">个人订阅</option>
|
||||||
<option value="team">Team / Business</option>
|
<option value="team">Team / Business</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
</label>
|
||||||
className="secondary"
|
<div className="account-buttons">
|
||||||
disabled={busy}
|
<button className="secondary" disabled={busy} onClick={() => login(x.id)}>
|
||||||
onClick={() => login(x.id)}
|
|
||||||
>
|
|
||||||
{busy && active === x.id ? "服务启动中…" : "设备码登录"}
|
{busy && active === x.id ? "服务启动中…" : "设备码登录"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="secondary"
|
className="secondary"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!confirm(`确定要退出“${x.displayName}”的 Codex 账号吗?`))
|
if (!confirm(`确定要退出“${x.displayName}”的 Codex 账号吗?`)) return;
|
||||||
return;
|
try {
|
||||||
|
setErr("");
|
||||||
await post(`accounts/${x.id}/logout`);
|
await post(`accounts/${x.id}/logout`);
|
||||||
load();
|
if (deviceLogin?.accountId === x.id) setDeviceLogin(null);
|
||||||
|
await load();
|
||||||
|
} catch (q) {
|
||||||
|
setErr((q as Error).message);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
@@ -834,35 +869,40 @@ function CodexSettings() {
|
|||||||
className="icon danger"
|
className="icon danger"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
title="删除连接"
|
title="删除连接"
|
||||||
|
aria-label={`删除“${x.displayName}”`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (
|
if (!confirm(`删除“${x.displayName}”及其全部凭据和历史数据?此操作无法撤销。`)) return;
|
||||||
confirm(
|
try {
|
||||||
`删除“${x.displayName}”及其全部凭据和历史数据?此操作无法撤销。`,
|
setErr("");
|
||||||
)
|
|
||||||
) {
|
|
||||||
await del(`accounts/${x.id}`);
|
await del(`accounts/${x.id}`);
|
||||||
if (+localStorage.accountId === x.id)
|
if (deviceLogin?.accountId === x.id) setDeviceLogin(null);
|
||||||
localStorage.removeItem("accountId");
|
if (active === x.id) setActive(0);
|
||||||
load();
|
if (+localStorage.accountId === x.id) localStorage.removeItem("accountId");
|
||||||
|
await load();
|
||||||
|
} catch (q) {
|
||||||
|
setErr((q as Error).message);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 />
|
<Trash2 />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
{result && (
|
{deviceLogin?.accountId === x.id && (
|
||||||
<div className="codebox">
|
<div className="codebox">
|
||||||
<span>
|
<span>
|
||||||
为“{xs.find((x) => x.id === active)?.displayName}”访问{" "}
|
在浏览器中访问{" "}
|
||||||
<a href={result.verificationUrl} target="_blank" rel="noreferrer">
|
<a href={deviceLogin.verificationUrl} target="_blank" rel="noreferrer">
|
||||||
{result.verificationUrl}
|
{deviceLogin.verificationUrl}
|
||||||
</a>
|
</a>
|
||||||
|
,然后输入设备码
|
||||||
</span>
|
</span>
|
||||||
<strong>{result.userCode}</strong>
|
<strong>{deviceLogin.userCode}</strong>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
{err && <p className="error">{err}</p>}
|
{err && <p className="error">{err}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+123
-32
@@ -443,18 +443,10 @@ header p {
|
|||||||
.checks input {
|
.checks input {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.actions,
|
.actions {
|
||||||
.settings-title {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.settings-title {
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
.settings-title p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.danger {
|
.danger {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--danger);
|
border: 1px solid var(--danger);
|
||||||
@@ -464,43 +456,126 @@ header p {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
.codebox {
|
.codebox {
|
||||||
padding: 16px;
|
grid-column: 1 / -1;
|
||||||
|
padding: 16px 18px;
|
||||||
border: 1px dashed var(--accent);
|
border: 1px dashed var(--accent);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
background: color-mix(in srgb, var(--accent) 6%, var(--panel));
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.codebox strong {
|
.codebox strong {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 20px;
|
font-size: 24px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.codex-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.codex-heading h2,
|
||||||
|
.add-account-card h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.codex-heading h2 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
.codex-heading p,
|
||||||
|
.add-account-card p {
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 7px 0 0;
|
||||||
|
}
|
||||||
|
.add-account-card {
|
||||||
|
padding: 22px 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.add-account-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.add-account-controls label {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
.add-account-controls button {
|
||||||
|
min-height: 43px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.account-list {
|
.account-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
.account-row {
|
.account-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(180px, 1fr) auto auto auto auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
gap: 10px;
|
gap: 20px 28px;
|
||||||
align-items: center;
|
padding: 22px 24px;
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
.account-row > div {
|
.account-card-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.account-card-main .dot {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.account-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 5px;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.account-row input {
|
.account-details input {
|
||||||
padding: 7px;
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
padding: 9px 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.account-row small {
|
.account-meta {
|
||||||
padding-left: 8px;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 16px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.account-meta span + span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.account-meta span + span::before {
|
||||||
|
content: "·";
|
||||||
|
position: absolute;
|
||||||
|
left: -10px;
|
||||||
|
color: var(--border);
|
||||||
|
}
|
||||||
|
.account-card-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 285px;
|
||||||
|
}
|
||||||
|
.account-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.account-buttons .secondary:first-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.account-empty {
|
||||||
|
padding: 44px 24px;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@@ -575,11 +650,30 @@ a {
|
|||||||
.fresh {
|
.fresh {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.account-row {
|
.add-account-card,
|
||||||
grid-template-columns: auto 1fr;
|
.add-account-controls {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.account-row button {
|
.add-account-controls label {
|
||||||
grid-column: auto;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.account-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
.account-card-actions {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.account-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.account-buttons .icon {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.codebox {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
.limit {
|
.limit {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -592,9 +686,6 @@ a {
|
|||||||
.limit footer {
|
.limit footer {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
.settings-title {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -111,3 +111,61 @@ test("programmatic tab changes do not clamp an existing scroll position", async
|
|||||||
await expect.poll(async () => (await layout(page)).scrollY).toBe(before.scrollY);
|
await expect.poll(async () => (await layout(page)).scrollY).toBe(before.scrollY);
|
||||||
await expect.poll(async () => (await layout(page)).content).toEqual(before.content);
|
await expect.poll(async () => (await layout(page)).content).toEqual(before.content);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("deleting a newly added account clears its device authorization", async ({ page }) => {
|
||||||
|
let accounts: Array<(typeof responses.accounts)[number]> = [];
|
||||||
|
await page.route("**/api/v1/**", async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
const key = new URL(request.url()).pathname.replace("/api/v1/", "");
|
||||||
|
if (key === "accounts" && request.method() === "GET")
|
||||||
|
return route.fulfill({ json: accounts });
|
||||||
|
if (key === "accounts" && request.method() === "POST") {
|
||||||
|
const account = {
|
||||||
|
...responses.accounts[0],
|
||||||
|
id: 2,
|
||||||
|
displayName: "账号 1",
|
||||||
|
email: "",
|
||||||
|
connected: false,
|
||||||
|
validationStatus: "pending" as const,
|
||||||
|
};
|
||||||
|
accounts = [account];
|
||||||
|
return route.fulfill({ json: account });
|
||||||
|
}
|
||||||
|
if (key === "accounts/2/login/device")
|
||||||
|
return route.fulfill({
|
||||||
|
json: {
|
||||||
|
verificationUrl: "https://auth.openai.com/codex/device",
|
||||||
|
userCode: "PLTJ-7M6I6",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (key === "accounts/2" && request.method() === "DELETE") {
|
||||||
|
accounts = [];
|
||||||
|
return route.fulfill({ json: { ok: true } });
|
||||||
|
}
|
||||||
|
return route.fulfill({ json: responses[key] ?? {} });
|
||||||
|
});
|
||||||
|
page.on("dialog", (dialog) => dialog.accept());
|
||||||
|
await page.goto("/settings");
|
||||||
|
await page.getByRole("tab", { name: "Codex" }).click();
|
||||||
|
await page.getByRole("button", { name: "添加账号" }).click();
|
||||||
|
await expect(page.getByText("PLTJ-7M6I6")).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "https://auth.openai.com/codex/device" })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "删除“账号 1”" }).click();
|
||||||
|
await expect(page.getByText("PLTJ-7M6I6")).toHaveCount(0);
|
||||||
|
await expect(page.getByRole("link", { name: "https://auth.openai.com/codex/device" })).toHaveCount(0);
|
||||||
|
await expect(page.getByText("尚未添加 Codex 账号", { exact: false })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Codex account cards fit within the viewport", async ({ page }) => {
|
||||||
|
await openSettings(page);
|
||||||
|
await page.getByRole("tab", { name: "Codex" }).click();
|
||||||
|
const overflow = await page.evaluate(() => ({
|
||||||
|
documentWidth: document.documentElement.scrollWidth,
|
||||||
|
viewportWidth: document.documentElement.clientWidth,
|
||||||
|
cardWidth: document.querySelector(".account-card")!.getBoundingClientRect().width,
|
||||||
|
contentWidth: document.querySelector(".settings-content")!.getBoundingClientRect().width,
|
||||||
|
}));
|
||||||
|
expect(overflow.documentWidth).toBeLessThanOrEqual(overflow.viewportWidth);
|
||||||
|
expect(overflow.cardWidth).toBeLessThanOrEqual(overflow.contentWidth);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user