Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ Changes:
- None yet

Fixes:
- None yet
- Stop attributing district docket report uploads to the per-tab cached
case id when the page context yields none: derive the id from the
sheet's own goDLS document links first. The cached id can belong to a
different case viewed earlier in the same tab, which uploaded one
case's entire docket sheet under another case's `pacer_case_id` and
merged it into the wrong archive docket.

For developers:
- Nothing yet
Expand Down
158 changes: 158 additions & 0 deletions spec/ContentDelegateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,27 @@ describe('The ContentDelegate class', function () {
await cd.handleDocketDisplayPage();
expect(dispatchBackgroundFetch).not.toHaveBeenCalled();
});

it('when a fresh tab has no storage entry at all', async function () {
const cd = new ContentDelegate(
tabId,
docketDisplayUrl,
undefined,
'canb',
undefined,
undefined,
[]
);
// A tab that never stored anything yields undefined, not {}.
window.chrome.storage.local.get = jasmine
.createSpy()
.and.callFake((_, cb) => {
cb({ options: { recap_enabled: true } });
});
dispatchBackgroundFetch = jasmine.createSpy();
await cd.handleDocketDisplayPage();
expect(dispatchBackgroundFetch).not.toHaveBeenCalled();
});
});

describe('when the history state is already set', function () {
Expand Down Expand Up @@ -596,6 +617,16 @@ describe('The ContentDelegate class', function () {
});

describe('when the docket page is not an interstitial page', function () {
const makeLink = (caseId, docId) => {
const a = document.createElement('a');
a.href = `https://ecf.canb.uscourts.gov/doc1/${docId}`;
a.setAttribute(
'onclick',
`goDLS('/doc1/${docId}','${caseId}','5','','','1','','','');` +
'return(false);'
);
return a;
};
beforeEach(function () {
clearDocumentBody();
table = document.createElement('table');
Expand Down Expand Up @@ -670,6 +701,133 @@ describe('The ContentDelegate class', function () {
expect(button.length).toBe(1);
});

it('prefers the goDLS case id over a stale tab id', async function () {
// The tab's cached caseId ('531591' in the storage mock)
// belongs to another case; the page's goDLS links must win.
const link = makeLink('177277', '034031424909');
const cd = new ContentDelegate(
tabId,
docketDisplayUrl,
docketDisplayPath,
'canb',
undefined, // no case id derived from url/referrer/inputs
undefined,
[link]
);
dispatchBackgroundNotifier = jasmine.createSpy();
dispatchBackgroundFetch = jasmine
.createSpy()
.and.callFake(fakeBackgroundFetch);
spyOn(history, 'replaceState');
await cd.handleDocketDisplayPage();
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
jasmine.objectContaining({
action: 'upload',
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
})
);
expect(dispatchBackgroundFetch).not.toHaveBeenCalledWith(
jasmine.objectContaining({
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
})
);
// the stale cached id is corrected for later pages in this tab
expect(window.chrome.storage.local.set).toHaveBeenCalledWith(
jasmine.objectContaining({
1234: jasmine.objectContaining({ caseId: '177277' }),
}),
jasmine.any(Function)
);
});

it('never overrides a context-derived case id', async function () {
// On a consolidated MEMBER docket the document links point at
// the LEAD case, so an id the page context provided (url,
// inputs, referrer) always beats the goDLS majority.
const link = makeLink('177277', '034031424909'); // lead case
const cd = new ContentDelegate(
tabId,
docketDisplayUrl,
docketDisplayPath,
'canb',
'531591', // the member case, from the page context
undefined,
[link]
);
dispatchBackgroundNotifier = jasmine.createSpy();
dispatchBackgroundFetch = jasmine
.createSpy()
.and.callFake(fakeBackgroundFetch);
spyOn(history, 'replaceState');
await cd.handleDocketDisplayPage();
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
jasmine.objectContaining({
action: 'upload',
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
})
);
});

it('uses the majority goDLS id on merged sheets', async function () {
// Consolidated/MDL dockets legitimately link member cases'
// documents — a stray link must not outvote the sheet.
const cd = new ContentDelegate(
tabId,
docketDisplayUrl,
docketDisplayPath,
'canb',
undefined,
undefined,
[
makeLink('177277', '034031424909'),
makeLink('177277', '034031424910'),
makeLink('177277', '034031424911'),
makeLink('999999', '034031424912'), // stray member-case link
]
);
dispatchBackgroundNotifier = jasmine.createSpy();
dispatchBackgroundFetch = jasmine
.createSpy()
.and.callFake(fakeBackgroundFetch);
spyOn(history, 'replaceState');
await cd.handleDocketDisplayPage();
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
jasmine.objectContaining({
action: 'upload',
data: jasmine.objectContaining({ pacer_case_id: '177277' }),
})
);
});

it('falls back when the goDLS ids are tied', async function () {
// Equal votes for two ids: don't guess; use the cached id
// ('531591' in the storage mock) as before.
const cd = new ContentDelegate(
tabId,
docketDisplayUrl,
docketDisplayPath,
'canb',
undefined,
undefined,
[
makeLink('177277', '034031424909'),
makeLink('888888', '034031424910'),
]
);
dispatchBackgroundNotifier = jasmine.createSpy();
dispatchBackgroundFetch = jasmine
.createSpy()
.and.callFake(fakeBackgroundFetch);
spyOn(history, 'replaceState');
await cd.handleDocketDisplayPage();
expect(dispatchBackgroundFetch).toHaveBeenCalledWith(
jasmine.objectContaining({
action: 'upload',
data: jasmine.objectContaining({ pacer_case_id: '531591' }),
})
);
});

it('calls uploadDocket and responds to a negative result', async function () {
const cd = docketDisplayContentDelegate;
dispatchBackgroundNotifier = jasmine.createSpy();
Expand Down
30 changes: 24 additions & 6 deletions src/content_delegate.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,30 @@ ContentDelegate.prototype.handleDocketDisplayPage = async function () {
// check if appellate
// let isAppellate = PACER.isAppellateCourt(this.court);

// if the content_delegate didn't pull the case Id on initialization,
// check the page for a lead case dktrpt url.
const tabStorage = await getItemsFromStorage(this.tabId);
this.pacer_case_id = this.pacer_case_id
? this.pacer_case_id
: tabStorage.caseId;
// If initialization yielded no case id, derive it from the page itself:
// the sheet's goDLS document links carry it. The per-tab cached id is
// the LAST resort — it can belong to a different case viewed earlier in
// this tab. An id the page context already provided is never overridden:
// consolidated member dockets legitimately link the lead case's
// documents, so the goDLS plurality only speaks when nothing else does.
if (!this.pacer_case_id) {
const pageCaseId = PACER.getCaseIdFromDocketDisplayLinks(this.links);
const tabStorage = (await getItemsFromStorage(this.tabId)) || {};
if (pageCaseId) {
if (tabStorage.caseId && tabStorage.caseId !== pageCaseId) {
console.warn(
`RECAP: Ignoring cached case id ${tabStorage.caseId}: the ` +
`docket's own links say it belongs to case ${pageCaseId}.`
);
}
this.pacer_case_id = pageCaseId;
// Seed the tab's cached case ID so a later docket-report load in this
// same tab starts with the correct case ID instead of a stale one.
await saveCaseIdinTabStorage({ tabId: this.tabId }, pageCaseId);
} else {
this.pacer_case_id = tabStorage.caseId;
}
}

// If we don't have this.pacer_case_id at this point, punt.
if (!this.pacer_case_id) return;
Expand Down
38 changes: 38 additions & 0 deletions src/pacer.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,44 @@ let PACER = {
}
},

// Returns the most common (plurality) `de_caseid` across the page's
// document links' `goDLS()` handlers. Plurality is intentional because
// consolidated/MDL docket sheets can legitimately link documents from
// multiple member cases. Returns `undefined` when there is no `goDLS`
// evidence or when the most common case IDs are tied.
getCaseIdFromDocketDisplayLinks: function (links) {
Comment thread
playfulart marked this conversation as resolved.
// Count how many document links point to each case ID. The case ID
// appearing most often is likely the case represented by the docket page.
const caseIdCounts = {};
for (const link of Array.from(links)) {
if (!PACER.isDoc1Url(link.href)) continue;

const goDLS = PACER.parseGoDLSFunction(link.getAttribute('onclick'));
if (goDLS && goDLS.de_caseid && goDLS.de_caseid !== '0') {
caseIdCounts[goDLS.de_caseid] =
(caseIdCounts[goDLS.de_caseid] ?? 0) + 1;
}
}

// Convert the counts object into an array of [caseId, count] pairs
// and sort descending so the most frequently linked case is first.
const sortedCaseIds = Object.entries(caseIdCounts).sort(
(a, b) => b[1] - a[1]
);

// No valid case IDs were found.
if (sortedCaseIds.length === 0) return undefined;

const [mostLinkedCaseId, mostLinkedCount] = sortedCaseIds[0];
const [, secondMostLinkedCount] = sortedCaseIds[1] ?? [];

// If multiple case IDs have the same highest count, we cannot determine
// which case is the correct one, so avoid returning an arbitrary result.
const hasTieForFirst = mostLinkedCount === secondMostLinkedCount;

return hasTieForFirst ? undefined : mostLinkedCaseId;
},

// Given a URL that satisfies isDocketQueryUrl, gets its case number.
getCaseNumberFromUrls: function (urls) {
// Iterate over an array of URLs and get the case number from the
Expand Down
Loading