Conservar progresso editorial entre renovações MFA e tornar a retoma visível
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-10 21:55:42 +02:00
parent d830fbca50
commit 4f3dbcf864
7 changed files with 308 additions and 79 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ final class FundingProgrammeReferences
'id' => $row?->id, 'published' => $row?->published_version_id !== null]; 'id' => $row?->id, 'published' => $row?->published_version_id !== null];
} }
return ['dataset_hash' => $package['sha256'], 'items' => $items]; return ['dataset_hash' => $package['sha256'], 'editor_id' => $actor->id, 'items' => $items];
} }
/** @return array{id:string,created:bool} */ /** @return array{id:string,created:bool} */
@@ -1,3 +1,15 @@
## 10/11/2026 — retoma após MFA, sem repetir fichas conferidas
Verificação activa posterior:90rascunhos/zero publicações; continuação GETMomentum403 às21:49:39 ePOSTpublicar403 às21:49:54 (+02). A sessão voltou a expirar. A publicação comunicada não chegou a ser executada pelo servidor. Nenhum rascunho foi removido.
Correcção delimitada emQA: a mesma aba conserva selecção, recibos/versionamento/hash e fundamento, separados por conta e conjunto de fontes; após reautenticação salta fichas já conferidas. As confirmações de revisão/publicação são desmarcadas na nova entrada, sem publicação automática. Uma publicação de resposta incerta conserva o mesmo corpo para recuperação idempotente. Novo comando de renovação utiliza logout/login/MFA nativos; limite900s e guardas mantidos. «Seleccionar todas as instituições(270)», contagem de progresso e controlos antes da lista recolhida tornam o estado visível. Sem migração, dados novos ou alterações de permissões. Alteração ainda não instalada nesta anotação; revisão activaef24cbdc.
Prova dirigida do componente efectivo: remonta a página como após autenticação, recupera selecção/recibos, não repete preparação, não publica automaticamente, recupera confirmação incerta e isola outra conta/fontes. Prova API editorial anterior1caso/27asserções reexecutada apenas pela propriedadeeditor_id acrescentada; provas270fichas e do dossier reutilizadas. Tipos/análise/compilação da entrega são registados pelo executor existente.
## 10/11/2026 — confirmação da preparação parcial e expiração do MFA
Após execução no editor, a homologação contém **32rascunhos e zero fichas publicadas**. A preparação gravou efectivamente e avançou por diferentes instituições. Os registos HTTP mostram preparação200 atéMyBucks às21:31:42, seguida deGETxtenda403 às21:31:43 ePOSTpublicar403 às21:31:59 (Africa/Johannesburg). A entrada no editor fora confirmada às21:16:43; corresponde ao limite existente de900segundos do MFA em `PortalProfileAccess::mfa`. Não é perda da instalação nem repetição de dados. Reautenticar no portal com MFA e retomar a preparação idempotente conserva os32rascunhos, sem substituir edições; publicação continua a exigir a confirmação separada. Nenhuma guarda, sessão ou permissão foi alterada. Revisão instalada `ef24cbdc`, ACT12 ainda sem conteúdo público confirmado. Prova de leitura: `outputs/funding-directory-20261110/institutions/publication-observation.json`.
## 10/11/2026 — instalação confirmada do directório financeiro ## 10/11/2026 — instalação confirmada do directório financeiro
Revisão `ef24cbdc9218e2027f7d1a39968cc60869a35410` instalada na homologação às19:15UTC, pelo executor Git existente.33ficheiros conferidos por hash com o commit; configuração preservada, zero migrações/importações automáticas. Backup/restauro confirmado de190tabelas e369ficheiros. Entradas `/fundos?kind=institution`, `/concursos`, `/login`, `/up` e revisão HTTP200. Pacote anterior preservado; nova entrega `outputs/entrega-instituicoes-editorial-20261110.zip` SHA256 `168d7ffb6d054533674f3f07799959b46bc12641c045accfc1766da5cead9077`. Revisão `ef24cbdc9218e2027f7d1a39968cc60869a35410` instalada na homologação às19:15UTC, pelo executor Git existente.33ficheiros conferidos por hash com o commit; configuração preservada, zero migrações/importações automáticas. Backup/restauro confirmado de190tabelas e369ficheiros. Entradas `/fundos?kind=institution`, `/concursos`, `/login`, `/up` e revisão HTTP200. Pacote anterior preservado; nova entrega `outputs/entrega-instituicoes-editorial-20261110.zip` SHA256 `168d7ffb6d054533674f3f07799959b46bc12641c045accfc1766da5cead9077`.
@@ -1,3 +1,15 @@
## 10/11/2026 — retoma após MFA, sem repetir fichas conferidas
Verificação activa posterior:90rascunhos/zero publicações; continuação GETMomentum403 às21:49:39 ePOSTpublicar403 às21:49:54 (+02). A sessão voltou a expirar. A publicação comunicada não chegou a ser executada pelo servidor. Nenhum rascunho foi removido.
Correcção delimitada emQA: a mesma aba conserva selecção, recibos/versionamento/hash e fundamento, separados por conta e conjunto de fontes; após reautenticação salta fichas já conferidas. As confirmações de revisão/publicação são desmarcadas na nova entrada, sem publicação automática. Uma publicação de resposta incerta conserva o mesmo corpo para recuperação idempotente. Novo comando de renovação utiliza logout/login/MFA nativos; limite900s e guardas mantidos. «Seleccionar todas as instituições(270)», contagem de progresso e controlos antes da lista recolhida tornam o estado visível. Sem migração, dados novos ou alterações de permissões. Alteração ainda não instalada nesta anotação; revisão activaef24cbdc.
Prova dirigida do componente efectivo: remonta a página como após autenticação, recupera selecção/recibos, não repete preparação, não publica automaticamente, recupera confirmação incerta e isola outra conta/fontes. Prova API editorial anterior1caso/27asserções reexecutada apenas pela propriedadeeditor_id acrescentada; provas270fichas e do dossier reutilizadas. Tipos/análise/compilação da entrega são registados pelo executor existente.
## 10/11/2026 — confirmação da preparação parcial e expiração do MFA
Após execução no editor, a homologação contém **32rascunhos e zero fichas publicadas**. A preparação gravou efectivamente e avançou por diferentes instituições. Os registos HTTP mostram preparação200 atéMyBucks às21:31:42, seguida deGETxtenda403 às21:31:43 ePOSTpublicar403 às21:31:59 (Africa/Johannesburg). A entrada no editor fora confirmada às21:16:43; corresponde ao limite existente de900segundos do MFA em `PortalProfileAccess::mfa`. Não é perda da instalação nem repetição de dados. Reautenticar no portal com MFA e retomar a preparação idempotente conserva os32rascunhos, sem substituir edições; publicação continua a exigir a confirmação separada. Nenhuma guarda, sessão ou permissão foi alterada. Revisão instalada `ef24cbdc`, ACT12 ainda sem conteúdo público confirmado. Prova de leitura: `outputs/funding-directory-20261110/institutions/publication-observation.json`.
## 10/11/2026 — instalação confirmada do directório financeiro ## 10/11/2026 — instalação confirmada do directório financeiro
Revisão `ef24cbdc9218e2027f7d1a39968cc60869a35410` instalada na homologação às19:15UTC, pelo executor Git existente.33ficheiros conferidos por hash com o commit; configuração preservada, zero migrações/importações automáticas. Backup/restauro confirmado de190tabelas e369ficheiros. Entradas `/fundos?kind=institution`, `/concursos`, `/login`, `/up` e revisão HTTP200. Pacote anterior preservado; nova entrega `outputs/entrega-instituicoes-editorial-20261110.zip` SHA256 `168d7ffb6d054533674f3f07799959b46bc12641c045accfc1766da5cead9077`. Revisão `ef24cbdc9218e2027f7d1a39968cc60869a35410` instalada na homologação às19:15UTC, pelo executor Git existente.33ficheiros conferidos por hash com o commit; configuração preservada, zero migrações/importações automáticas. Backup/restauro confirmado de190tabelas e369ficheiros. Entradas `/fundos?kind=institution`, `/concursos`, `/login`, `/up` e revisão HTTP200. Pacote anterior preservado; nova entrega `outputs/entrega-instituicoes-editorial-20261110.zip` SHA256 `168d7ffb6d054533674f3f07799959b46bc12641c045accfc1766da5cead9077`.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,15 @@
## 10/11/2026 — retoma após MFA, sem repetir fichas conferidas
Verificação activa posterior:90rascunhos/zero publicações; continuação GETMomentum403 às21:49:39 ePOSTpublicar403 às21:49:54 (+02). A sessão voltou a expirar. A publicação comunicada não chegou a ser executada pelo servidor. Nenhum rascunho foi removido.
Correcção delimitada emQA: a mesma aba conserva selecção, recibos/versionamento/hash e fundamento, separados por conta e conjunto de fontes; após reautenticação salta fichas já conferidas. As confirmações de revisão/publicação são desmarcadas na nova entrada, sem publicação automática. Uma publicação de resposta incerta conserva o mesmo corpo para recuperação idempotente. Novo comando de renovação utiliza logout/login/MFA nativos; limite900s e guardas mantidos. «Seleccionar todas as instituições(270)», contagem de progresso e controlos antes da lista recolhida tornam o estado visível. Sem migração, dados novos ou alterações de permissões. Alteração ainda não instalada nesta anotação; revisão activaef24cbdc.
Prova dirigida do componente efectivo: remonta a página como após autenticação, recupera selecção/recibos, não repete preparação, não publica automaticamente, recupera confirmação incerta e isola outra conta/fontes. Prova API editorial anterior1caso/27asserções reexecutada apenas pela propriedadeeditor_id acrescentada; provas270fichas e do dossier reutilizadas. Tipos/análise/compilação da entrega são registados pelo executor existente.
## 10/11/2026 — confirmação da preparação parcial e expiração do MFA
Após execução no editor, a homologação contém **32rascunhos e zero fichas publicadas**. A preparação gravou efectivamente e avançou por diferentes instituições. Os registos HTTP mostram preparação200 atéMyBucks às21:31:42, seguida deGETxtenda403 às21:31:43 ePOSTpublicar403 às21:31:59 (Africa/Johannesburg). A entrada no editor fora confirmada às21:16:43; corresponde ao limite existente de900segundos do MFA em `PortalProfileAccess::mfa`. Não é perda da instalação nem repetição de dados. Reautenticar no portal com MFA e retomar a preparação idempotente conserva os32rascunhos, sem substituir edições; publicação continua a exigir a confirmação separada. Nenhuma guarda, sessão ou permissão foi alterada. Revisão instalada `ef24cbdc`, ACT12 ainda sem conteúdo público confirmado. Prova de leitura: `outputs/funding-directory-20261110/institutions/publication-observation.json`.
## 10/11/2026 — instalação confirmada do directório financeiro ## 10/11/2026 — instalação confirmada do directório financeiro
Revisão `ef24cbdc9218e2027f7d1a39968cc60869a35410` instalada na homologação às19:15UTC, pelo executor Git existente.33ficheiros conferidos por hash com o commit; configuração preservada, zero migrações/importações automáticas. Backup/restauro confirmado de190tabelas e369ficheiros. Entradas `/fundos?kind=institution`, `/concursos`, `/login`, `/up` e revisão HTTP200. Pacote anterior preservado; nova entrega `outputs/entrega-instituicoes-editorial-20261110.zip` SHA256 `168d7ffb6d054533674f3f07799959b46bc12641c045accfc1766da5cead9077`. Revisão `ef24cbdc9218e2027f7d1a39968cc60869a35410` instalada na homologação às19:15UTC, pelo executor Git existente.33ficheiros conferidos por hash com o commit; configuração preservada, zero migrações/importações automáticas. Backup/restauro confirmado de190tabelas e369ficheiros. Entradas `/fundos?kind=institution`, `/concursos`, `/login`, `/up` e revisão HTTP200. Pacote anterior preservado; nova entrega `outputs/entrega-instituicoes-editorial-20261110.zip` SHA256 `168d7ffb6d054533674f3f07799959b46bc12641c045accfc1766da5cead9077`.
+213 -69
View File
@@ -1,5 +1,5 @@
import { Link } from "@inertiajs/react"; import { Link, router } from "@inertiajs/react";
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
financialCategories, financialCategories,
financialTypes, financialTypes,
@@ -27,21 +27,82 @@ type Receipt = {
published?: boolean; published?: boolean;
}; };
export default function Assisted({ type Progress = {
selected: string[];
receipts: Record<string, Receipt>;
reason: string;
pending: Record<string, object>;
};
const emptyProgress = (): Progress => ({
selected: [],
receipts: {},
reason: "",
pending: {},
});
export function readProgress(key: string): Progress {
try {
const value = JSON.parse(sessionStorage.getItem(key) ?? "null");
if (
!value ||
!Array.isArray(value.selected) ||
!value.selected.every((c: unknown) => typeof c === "string") ||
!value.receipts ||
typeof value.receipts !== "object" ||
typeof value.reason !== "string" ||
!value.pending ||
typeof value.pending !== "object"
)
return emptyProgress();
return value;
} catch {
return emptyProgress();
}
}
function saveProgress(key: string, value: Progress): void {
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch {
/* Native receipts remain authoritative if tab storage is unavailable. */
}
}
export function AssistedEditor({
batch, batch,
}: { }: {
batch: { dataset_hash: string; items: Item[] }; batch: { dataset_hash: string; editor_id: number; items: Item[] };
}) { }) {
const [selected, setSelected] = useState<string[]>([]); const storageKey = `investmoz-funding-progress:${batch.editor_id}:${batch.dataset_hash}`;
const [receipts, setReceipts] = useState<Record<string, Receipt>>({}); const [restored] = useState(() => readProgress(storageKey));
const [selected, setSelected] = useState<string[]>(
restored.selected.filter((c) => batch.items.some((i) => i.code === c)),
);
const [receipts, setReceipts] = useState<Record<string, Receipt>>(
restored.receipts,
);
const [reviewed, setReviewed] = useState(false); const [reviewed, setReviewed] = useState(false);
const [confirmed, setConfirmed] = useState(false); const [confirmed, setConfirmed] = useState(false);
const [reason, setReason] = useState(""); const [reason, setReason] = useState(restored.reason);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [message, setMessage] = useState(""); const [message, setMessage] = useState("");
const running = useRef(false); const running = useRef(false);
const stop = useRef(false); const stop = useRef(false);
const pending = useRef<Record<string, object>>({}); const pending = useRef<Record<string, object>>(restored.pending);
const [needsMfa, setNeedsMfa] = useState(false);
useEffect(() => {
saveProgress(storageKey, {
selected,
receipts,
reason,
pending: pending.current,
});
}, [storageKey, selected, receipts, reason]);
const ready = selected.filter(
(c) =>
receipts[c]?.matches_reference &&
receipts[c]?.publication &&
!receipts[c]?.published,
).length;
const published = selected.filter((c) => receipts[c]?.published).length;
async function run(publish: boolean) { async function run(publish: boolean) {
if ( if (
running.current || running.current ||
@@ -57,6 +118,7 @@ export default function Assisted({
try { try {
for (const code of selected) { for (const code of selected) {
if (stop.current) break; if (stop.current) break;
if (!publish && receipts[code]) continue;
setMessage( setMessage(
`${publish ? "Publicação" : "Preparação"}: ${batch.items.find((i) => i.code === code)?.title}`, `${publish ? "Publicação" : "Preparação"}: ${batch.items.find((i) => i.code === code)?.title}`,
); );
@@ -74,6 +136,12 @@ export default function Assisted({
confirm: true, confirm: true,
}; };
pending.current[code] = body; pending.current[code] = body;
saveProgress(storageKey, {
selected,
receipts,
reason,
pending: pending.current,
});
await profileJson( await profileJson(
`/management/fundos/${receipt.id}/publicar`, `/management/fundos/${receipt.id}/publicar`,
body, body,
@@ -108,6 +176,14 @@ export default function Assisted({
: "Preparação concluída. Confira os resultados antes de confirmar a publicação.", : "Preparação concluída. Confira os resultados antes de confirmar a publicação.",
); );
} catch (error) { } catch (error) {
setNeedsMfa(
!!error &&
typeof error === "object" &&
"status" in error &&
(error.status === 401 ||
error.status === 403 ||
error.status === 419),
);
setMessage( setMessage(
`${error instanceof Error ? error.message : "Operação interrompida."} Os resultados anteriores foram conservados. Se a sessão ou MFA expirou, autentique-se no portal e retome. A publicação incerta pode ser repetida com a mesma confirmação.`, `${error instanceof Error ? error.message : "Operação interrompida."} Os resultados anteriores foram conservados. Se a sessão ou MFA expirou, autentique-se no portal e retome. A publicação incerta pode ser repetida com a mesma confirmação.`,
); );
@@ -145,71 +221,94 @@ export default function Assisted({
setConfirmed(false); setConfirmed(false);
}} }}
> >
Seleccionar instituições do documento Seleccionar todas as instituições (270)
</button> </button>
<p>{selected.length} seleccionadas</p> <p role="status">
{batch.items.map((item) => ( {selected.length} seleccionadas ·{" "}
<details key={item.code} className="border-b py-2"> {Object.keys(receipts).length} preparadas nesta retoma ·{" "}
<summary> {ready} prontas para publicar · {published} publicadas
<label> </p>
<input <p>
type="checkbox" O progresso fica guardado nesta aba, separado por conta e
checked={selected.includes(item.code)} fontes. Depois de renovar o MFA, retome os passos sem voltar
onChange={(e) => { a preparar as fichas conferidas. As confirmações devem ser
setSelected((old) => marcadas novamente; nenhuma publicação é automática.
e.target.checked </p>
? [...old, item.code] <details>
: old.filter( <summary>
(c) => c !== item.code, Conferir a lista e alterar a selecção (
), {batch.items.length} referências)
); </summary>
setConfirmed(false); {batch.items.map((item) => (
}} <details key={item.code} className="border-b py-2">
/>{" "} <summary>
{item.title} <label>
</label>{" "} <input
—{" "} type="checkbox"
{receipts[item.code]?.published checked={selected.includes(item.code)}
? "Publicação confirmada" onChange={(e) => {
: receipts[item.code] setSelected((old) =>
? receipts[item.code].matches_reference e.target.checked
? "Conferida, aguarda publicação" ? [...old, item.code]
: "Edições existentes: rever individualmente" : old.filter(
: item.published (c) =>
? "Já tem publicação" c !== item.code,
: "Por preparar"} ),
</summary> );
<p>{item.summary}</p> setConfirmed(false);
<p> }}
{item.categories.map(c => financialCategories[c] ?? c).join(", ")} —{" "} />{" "}
{item.types.map(t => financialTypes[t] ?? t).join(", ")} — {item.status} {item.title}
</p> </label>{" "}
<p> —{" "}
{item.source_document}{" "} {receipts[item.code]?.published
<code className="break-all"> ? "Publicação confirmada"
{item.source_sha256} : receipts[item.code]
</code> ? receipts[item.code].matches_reference
</p> ? "Conferida, aguarda publicação"
<p> : "Edições existentes: rever individualmente"
{item.locations.length} localizações;{" "} : item.published
{item.contacts.length} contactos. ? "Já tem publicação"
</p> : "Por preparar"}
<Link </summary>
className="underline" <p>{item.summary}</p>
href={`/management/fundos/referencias/${item.code}`} <p>
> {item.categories
Conferir fonte e dados completos .map((c) => financialCategories[c] ?? c)
</Link> .join(", ")}{" "}
{receipts[item.code] && ( —{" "}
{item.types
.map((t) => financialTypes[t] ?? t)
.join(", ")}{" "}
— {item.status}
</p>
<p>
{item.source_document}{" "}
<code className="break-all">
{item.source_sha256}
</code>
</p>
<p>
{item.locations.length} localizações;{" "}
{item.contacts.length} contactos.
</p>
<Link <Link
className="ml-4 underline" className="underline"
href={`/management/fundos/${receipts[item.code].id}`} href={`/management/fundos/referencias/${item.code}`}
> >
Alterar ficha existente Conferir fonte e dados completos
</Link> </Link>
)} {receipts[item.code] && (
</details> <Link
))} className="ml-4 underline"
href={`/management/fundos/${receipts[item.code].id}`}
>
Alterar ficha existente
</Link>
)}
</details>
))}
</details>
<label className="block py-3"> <label className="block py-3">
<input <input
type="checkbox" type="checkbox"
@@ -245,6 +344,11 @@ export default function Assisted({
Confirmo a publicação pública das versões conferidas, Confirmo a publicação pública das versões conferidas,
preservando os dados e as incertezas apresentados. preservando os dados e as incertezas apresentados.
</label> </label>
<p>
Para publicar: termine a preparação, marque a conferência,
preencha o fundamento e confirme a publicação. Fichas com
edições existentes exigem revisão individual.
</p>
<button <button
type="button" type="button"
className={button} className={button}
@@ -263,6 +367,33 @@ export default function Assisted({
2. Publicar conferidas 2. Publicar conferidas
</button> </button>
</fieldset> </fieldset>
{needsMfa && (
<div role="alert" className={card}>
<p>
A sessão ou confirmação MFA precisa de renovação. O
progresso desta aba foi conservado.
</p>
<button
className={button}
type="button"
disabled={busy}
onClick={() =>
router.post(
"/logout",
{},
{
onSuccess: () =>
router.visit(
"/management/fundos/referencias-assistidas",
),
},
)
}
>
Renovar entrada com MFA e retomar
</button>
</div>
)}
{busy && ( {busy && (
<button <button
type="button" type="button"
@@ -283,3 +414,16 @@ export default function Assisted({
</Frame> </Frame>
); );
} }
export default function Assisted({
batch,
}: {
batch: { dataset_hash: string; editor_id: number; items: Item[] };
}) {
return (
<AssistedEditor
key={`${batch.editor_id}:${batch.dataset_hash}`}
batch={batch}
/>
);
}
+53 -4
View File
@@ -124,7 +124,13 @@ const text = (node) =>
? text(node.props?.children) ? text(node.props?.children)
: String(node ?? ""); : String(node ?? "");
test("assisted editorial preparation never publishes until a separate confirmation and retries the same publication", async () => { test("assisted editor resumes after renewed authentication without preparing completed fiches or automatically publishing", async () => {
const previousStorage = globalThis.sessionStorage;
const tab = new Map();
globalThis.sessionStorage = {
getItem: (key) => tab.get(key) ?? null,
setItem: (key, value) => tab.set(key, value),
};
const states = [], const states = [],
refs = [], refs = [],
calls = []; calls = [];
@@ -133,9 +139,13 @@ test("assisted editorial preparation never publishes until a separate confirmati
lost = true; lost = true;
const hooks = { const hooks = {
...React, ...React,
useEffect(fn) {
fn();
},
useState(initial) { useState(initial) {
const i = si++; const i = si++;
if (!(i in states)) states[i] = initial; if (!(i in states))
states[i] = typeof initial === "function" ? initial() : initial;
return [ return [
states[i], states[i],
(v) => { (v) => {
@@ -175,10 +185,11 @@ test("assisted editorial preparation never publishes until a separate confirmati
return { package_sha256: "qa-dataset", hash: "qa-source" }; return { package_sha256: "qa-dataset", hash: "qa-source" };
}, },
}, },
}).default; }).AssistedEditor;
const props = { const props = {
batch: { batch: {
dataset_hash: "qa-dataset", dataset_hash: "qa-dataset",
editor_id: 42,
items: [ items: [
{ {
code: "qa", code: "qa",
@@ -214,6 +225,23 @@ test("assisted editorial preparation never publishes until a separate confirmati
assert.equal(calls.length, 2); assert.equal(calls.length, 2);
assert.ok(calls.every((c) => !c.url.endsWith("/publicar"))); assert.ok(calls.every((c) => !c.url.endsWith("/publicar")));
tree = render(); tree = render();
states.length = 0;
refs.length = 0; // Full page remount after login/MFA in the same tab.
tree = render();
assert.match(text(tree), /1 seleccionadas/);
assert.match(text(tree), /1 preparadas nesta retoma/);
boxes = flatten(tree).filter((n) => n.type === "input");
assert.equal(boxes[1].props.checked, false);
boxes[1].props.onChange({ target: { checked: true } });
tree = render();
click(tree, "1.");
await new Promise((resolve) => setImmediate(resolve));
assert.equal(
calls.length,
2,
"A completed fiche must not be prepared again",
);
tree = render();
assert.equal( assert.equal(
flatten(tree).find( flatten(tree).find(
(n) => n.type === "button" && text(n).startsWith("2."), (n) => n.type === "button" && text(n).startsWith("2."),
@@ -246,6 +274,25 @@ test("assisted editorial preparation never publishes until a separate confirmati
assert.deepEqual(calls[3], calls[2]); assert.deepEqual(calls[3], calls[2]);
assert.equal(calls[2].body.confirm, true); assert.equal(calls[2].body.confirm, true);
assert.match(text(render()), /Publicação confirmada/); assert.match(text(render()), /Publicação confirmada/);
props.batch.editor_id = 43;
states.length = 0;
refs.length = 0;
assert.match(
text(render()),
/0 seleccionadas/,
"Another account must not inherit progress",
);
props.batch.editor_id = 42;
props.batch.dataset_hash = "changed-source";
states.length = 0;
refs.length = 0;
assert.match(
text(render()),
/0 seleccionadas/,
"Changed sources must not inherit receipts",
);
if (previousStorage === undefined) delete globalThis.sessionStorage;
else globalThis.sessionStorage = previousStorage;
}); });
test("actual dossier continuation freezes a lost response retry and retains dossier/opportunity and selected product", async () => { test("actual dossier continuation freezes a lost response retry and retains dossier/opportunity and selected product", async () => {
const states = [], const states = [],
@@ -255,9 +302,11 @@ test("actual dossier continuation freezes a lost response retry and retains doss
const calls = []; const calls = [];
const hooks = { const hooks = {
...React, ...React,
useEffect() {},
useState(initial) { useState(initial) {
const i = stateIndex++; const i = stateIndex++;
if (!(i in states)) states[i] = initial; if (!(i in states))
states[i] = typeof initial === "function" ? initial() : initial;
return [states[i], (value) => (states[i] = value)]; return [states[i], (value) => (states[i] = value)];
}, },
useRef(initial) { useRef(initial) {