Skip to content

Conversation

eric-richardson1
Copy link
Collaborator

@eric-richardson1 eric-richardson1 commented Sep 17, 2025

This PR adds functionality to support the dolt_tests system table and dolt_test_run() table function. This includes the new "Tests" tab, which allows you to view, edit, and run tests, as well as a test runner component that allows you to run tests and view test results before merging a branch.

@eric-richardson1 eric-richardson1 marked this pull request as draft September 17, 2025 16:50
@eric-richardson1 eric-richardson1 marked this pull request as ready for review September 18, 2025 22:38
Copy link
Collaborator

@tbantle22 tbantle22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far, started with the graphql server code

Copy link
Collaborator

@tbantle22 tbantle22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few higher level comments before I get into the details

Copy link
Collaborator

@tbantle22 tbantle22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking much better

I know you have your blog today and most of these comments are best practice related and non blocking. If needed they can be addressed after you get this all in

The one blocking one is making sure that if runTests results in an error at any point that that error is displayed on the page

Also, have you tested this with doltgres? If it doesn't work you should hide this tab for doltgres databases

Comment on lines +20 to +30
expandedItems: Set<string>;
expandedGroups: Set<string>;
emptyGroups: Set<string>;
editingTestNames: Record<string, string>;
tests: Test[];
groupedTests: Record<string, Test[]>;
sortedGroupEntries: Array<[string, Test[]]>;
testResults: Record<
string,
{ status: "passed" | "failed"; error?: string } | undefined
>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you return state: TestState from the context so you don't need to list all these types out twice?

Comment on lines +19 to +20
flex: 1 1 auto;
min-width: 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use tailwind classes for all these in this file too

Comment on lines +82 to +85
@apply w-full !important;
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should avoid using !important whenever possible. It gets messy quickly. There are usually other ways to make it work

Comment on lines +32 to +127
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [localAssertionValue, setLocalAssertionValue] = useState(
test.assertionValue,
);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const updateTest = useCallback(
(name: string, field: keyof Test, value: string) => {
setState({
tests: tests.map((test: Test) =>
test.testName === name ? { ...test, [field]: value } : test,
),
hasUnsavedChanges: true,
});
},
[tests, setState],
);

const handleDeleteTest = (testName: string) => {
const newExpandedItems = new Set(expandedItems);
newExpandedItems.delete(testName);
setState({
tests: tests.filter(test => test.testName !== testName),
expandedItems: newExpandedItems,
hasUnsavedChanges: true,
});
};

const handleTestNameEdit = (testId: string, name: string) => {
setState({
editingTestNames: {
...editingTestNames,
[testId]: name,
},
});
};

const handleTestNameBlur = (testName: string) => {
const newName = editingTestNames[testName];
const test = tests.find(t => t.testName === testName);
if (newName.trim() && newName !== test?.testName) {
updateTest(testName, "testName", newName.trim());
}
const newEditingTestNames = { ...editingTestNames };
delete newEditingTestNames[testName];
setState({ editingTestNames: newEditingTestNames });
};

const groupOptions = sortedGroupEntries
.map(entry => entry[0])
.filter(group => group !== "");
const isExpanded = expandedItems.has(test.testName);
const editingName = editingTestNames[test.testName];
const testResult = testResults[test.testName];

const debouncedOnUpdateTest = (field: keyof Test, value: string) => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
updateTest(test.testName, field, value);
}, 500); // 500ms debounce
};

useEffect(
() => () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
},
[],
);

useEffect(() => {
setLocalAssertionValue(test.assertionValue);
}, [test.assertionValue]);

const handleDeleteClick = (e: MouseEvent) => {
e.stopPropagation();
setShowDeleteConfirm(true);
};

const handleConfirmDelete = () => {
setShowDeleteConfirm(false);
handleDeleteTest(test.testName);
};

const handleCancelDelete = () => {
setShowDeleteConfirm(false);
};

const handleAssertionValueBlur = () => {
if (localAssertionValue !== test.assertionValue) {
updateTest(test.testName, "assertionValue", localAssertionValue);
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the group item comment, this could be moved into a useEditTestItem hook in this dir

<div className={css.expandedContent}>
{testResult?.status === "failed" && testResult.error && (
<div className={css.errorMessage}>
<strong>Error:</strong> {testResult.error}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer using a span with a className here over strong

Copy link
Collaborator

@tbantle22 tbantle22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some error handling related comments, but none of them are blocking

Comment on lines +328 to +330
<span style={{ color: "red" }}>
Failed to load tests: {error.message}
</span>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use ErrorMsg for styling

Comment on lines +382 to +384
<div style={{ color: "red", padding: "8px" }}>
Error running tests: {runTestError || runTestsError?.message}
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too. And should generally avoid using inline styling like this and use classes instead

});

if (result.error) {
console.error("Error running tests:", result.error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to console the error if you're displaying the error

const [testResults, setTestResults] = useState<TestResult[]>([]);
const [runTests] = useRunTestsLazyQuery();
const { data } = useTestListQuery({
const [runTestError, setRunTestError] = useState<string | null>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useApolloError might simplify some of the type casting below

Comment on lines +337 to +339
if (runTestsError) {
console.error("Run Tests query error:", runTestsError);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this

Comment on lines +70 to +74
useEffect(() => {
if (testsError) {
console.error("Error loading tests:", testsError);
}
}, [testsError]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this

result.data &&
result.data.runTests.list.length > 0 &&
result.data.runTests.list[0].status === "PASS";
{} as Record<string, { status: "passed" | "failed"; error?: string }>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could make { status: "passed" | "failed"; error?: string } a named type since it's repeated a few times

Comment on lines 262 to 271
result.data?.runTests.list.map(test =>
test.status === "PASS"
? {
status: "passed",
}
: {
status: "failed",
error: test.message,
},
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be assigned to something?

<Button>Commit</Button>
</Link>
</div>
{!testsLoading && !testsError && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should put all of this in an Inner component and use this pattern that we use in other components:

if (loading) return <Loader />
if (error) return <ErrorMsg />
return <div>{...inner test stuff that relies on query results}</div>

There's also a QueryHandler util component that does this for you

@eric-richardson1 eric-richardson1 merged commit 8de08a5 into main Sep 23, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants