diff --git a/.changeset/modern-students-tie.md b/.changeset/modern-students-tie.md new file mode 100644 index 0000000000..8a15f22ec7 --- /dev/null +++ b/.changeset/modern-students-tie.md @@ -0,0 +1,5 @@ +--- +'houdini-react': patch +--- + +Fix $handle reference diff --git a/e2e/_api/graphql.mjs b/e2e/_api/graphql.mjs index 6f2b3c1177..2a893fc501 100644 --- a/e2e/_api/graphql.mjs +++ b/e2e/_api/graphql.mjs @@ -67,7 +67,7 @@ let monkeys = [ ] // example data -const dataUsers = [ +export const dataUsers = [ { id: '1', name: 'Bruce Willis', @@ -135,7 +135,7 @@ const listA = [] const listB = [] const userSnapshots = {} -function getUserSnapshot(snapshot) { +export function getUserSnapshot(snapshot) { if (!userSnapshots[snapshot]) { userSnapshots[snapshot] = dataUsers.map((user) => ({ ...user, @@ -325,6 +325,9 @@ export const resolvers = { return null }, + avatarURL: (user, { size }) => { + return !size ? user.avatarURL : user.avatarURL + `?size=${size}` + }, }, Mutation: { diff --git a/e2e/_api/graphql.mjs.d.ts b/e2e/_api/graphql.mjs.d.ts new file mode 100644 index 0000000000..2596b15315 --- /dev/null +++ b/e2e/_api/graphql.mjs.d.ts @@ -0,0 +1,9 @@ +export type User = { + id: string + name: string + birthDate: Date + avatarURL: string +} + +export const dataUsers: User[] = [] +export function getUserSnapshot(snapshot: string): User {} diff --git a/e2e/react/.gitignore b/e2e/react/.gitignore index f61753d63d..a43c386044 100644 --- a/e2e/react/.gitignore +++ b/e2e/react/.gitignore @@ -25,3 +25,5 @@ dist-ssr *.sln *.sw? vite.config.ts.* + +test-results/ diff --git a/e2e/react/package.json b/e2e/react/package.json index 23385e2aa1..d8fa61f143 100644 --- a/e2e/react/package.json +++ b/e2e/react/package.json @@ -14,8 +14,9 @@ "web": "vite dev", "dev": "concurrently \"pnpm run web\" \"pnpm run api\" -n \"web,api\" -c \"green,magenta\"", "build": "vite build", - "tests": "playwright test ", + "tests": "playwright test", "test": "npm run tests", + "tw": "npx tailwindcss -i ./src/styles.css -o ./public/assets/output.css --watch", "preview": "vite dev" }, "dependencies": { diff --git a/e2e/react/playwright.config.ts b/e2e/react/playwright.config.ts index 90556d762e..814f9f959f 100644 --- a/e2e/react/playwright.config.ts +++ b/e2e/react/playwright.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ testIgnore: '**/$houdini/**', webServer: { - command: 'npm run dev -- --port 3008', + command: 'PORT=3008 npm run dev', port: 3008, timeout: 120 * 1000, }, diff --git a/e2e/react/public/assets/output.css b/e2e/react/public/assets/output.css new file mode 100644 index 0000000000..83ca340122 --- /dev/null +++ b/e2e/react/public/assets/output.css @@ -0,0 +1,1471 @@ +/* +! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.static { + position: static; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.block { + display: block; +} + +.flex { + display: flex; +} + +.w-full { + width: 100%; +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.gap-12 { + gap: 3rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.border-2 { + border-width: 2px; +} + +.border-solid { + border-style: solid; +} + +.border-white { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + +.border-\[var\(--links\)\] { + border-color: var(--links); +} + +.p-4 { + padding: 1rem; +} + +.p-2 { + padding: 0.5rem; +} + +/** + * Forced dark theme version + */ + +:root { + --background-body: #202b38; + --background: #161f27; + --background-alt: #1a242f; + --selection: #1c76c5; + --text-main: #dbdbdb; + --text-bright: #fff; + --text-muted: #a9b1ba; + --links: #41adff; + --focus: #0096bfab; + --border: #526980; + --code: #ffbe85; + --animation-duration: 0.1s; + --button-base: #0c151c; + --button-hover: #040a0f; + --scrollbar-thumb: var(--button-hover); + --scrollbar-thumb-hover: rgb(0, 0, 0); + --form-placeholder: #a9a9a9; + --form-text: #fff; + --variable: #d941e2; + --highlight: #efdb43; + --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E"); +} + +html { + scrollbar-color: #040a0f #202b38; + scrollbar-color: var(--scrollbar-thumb) var(--background-body); + scrollbar-width: thin; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; + line-height: 1.4; + max-width: 800px; + margin: 20px auto; + padding: 0 10px; + word-wrap: break-word; + color: #dbdbdb; + color: var(--text-main); + background: #202b38; + background: var(--background-body); + text-rendering: optimizeLegibility; +} + +button { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +input { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +textarea { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; +} + +h1 { + font-size: 2.2em; + margin-top: 0; +} + +h1, + h2, + h3, + h4, + h5, + h6 { + margin-bottom: 12px; + margin-top: 24px; +} + +h1 { + color: #fff; + color: var(--text-bright); +} + +h2 { + color: #fff; + color: var(--text-bright); +} + +h3 { + color: #fff; + color: var(--text-bright); +} + +h4 { + color: #fff; + color: var(--text-bright); +} + +h5 { + color: #fff; + color: var(--text-bright); +} + +h6 { + color: #fff; + color: var(--text-bright); +} + +strong { + color: #fff; + color: var(--text-bright); +} + +h1, + h2, + h3, + h4, + h5, + h6, + b, + strong, + th { + font-weight: 600; +} + +q::before { + content: none; +} + +q::after { + content: none; +} + +blockquote { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +q { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; +} + +blockquote > footer { + font-style: normal; + border: 0; +} + +blockquote cite { + font-style: normal; +} + +address { + font-style: normal; +} + +a[href^='mailto\:']::before { + content: '📧 '; +} + +a[href^='tel\:']::before { + content: '📞 '; +} + +a[href^='sms\:']::before { + content: '💬 '; +} + +mark { + background-color: #efdb43; + background-color: var(--highlight); + border-radius: 2px; + padding: 0 2px 0 2px; + color: #000; +} + +a > code, + a > strong { + color: inherit; +} + +button, + select, + input[type='submit'], + input[type='reset'], + input[type='button'], + input[type='checkbox'], + input[type='range'], + input[type='radio'] { + cursor: pointer; +} + +input, + select { + display: block; +} + +[type='checkbox'], + [type='radio'] { + display: initial; +} + +input { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +textarea { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +select { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; +} + +button { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='submit'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='reset'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +input[type='button'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; +} + +button:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='submit']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='reset']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='button']:hover { + background: #040a0f; + background: var(--button-hover); +} + +input[type='color'] { + min-height: 2rem; + padding: 8px; + cursor: pointer; +} + +input[type='checkbox'], + input[type='radio'] { + height: 1em; + width: 1em; +} + +input[type='radio'] { + border-radius: 100%; +} + +input { + vertical-align: top; +} + +label { + vertical-align: middle; + margin-bottom: 4px; + display: inline-block; +} + +input:not([type='checkbox']):not([type='radio']), + input[type='range'], + select, + button, + textarea { + -webkit-appearance: none; +} + +textarea { + display: block; + margin-right: 0; + box-sizing: border-box; + resize: vertical; +} + +textarea:not([cols]) { + width: 100%; +} + +textarea:not([rows]) { + min-height: 40px; + height: 140px; +} + +select { + background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; + background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; + padding-right: 35px; +} + +select::-ms-expand { + display: none; +} + +select[multiple] { + padding-right: 10px; + background-image: none; + overflow-y: auto; +} + +input:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +select:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +button:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +textarea:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); +} + +input[type='checkbox']:active, + input[type='radio']:active, + input[type='submit']:active, + input[type='reset']:active, + input[type='button']:active, + input[type='range']:active, + button:active { + transform: translateY(2px); +} + +input:disabled, + select:disabled, + button:disabled, + textarea:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +::-moz-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +::placeholder { + color: #a9a9a9; + color: var(--form-placeholder); +} + +fieldset { + border: 1px #0096bfab solid; + border: 1px var(--focus) solid; + border-radius: 6px; + margin: 0; + margin-bottom: 12px; + padding: 10px; +} + +legend { + font-size: 0.9em; + font-weight: 600; +} + +input[type='range'] { + margin: 10px 0; + padding: 10px 0; + background: transparent; +} + +input[type='range']:focus { + outline: none; +} + +input[type='range']::-webkit-slider-runnable-track { + width: 100%; + height: 9.5px; + -webkit-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-webkit-slider-thumb { + box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + -webkit-appearance: none; + margin-top: -7px; +} + +input[type='range']:focus::-webkit-slider-runnable-track { + background: #161f27; + background: var(--background); +} + +input[type='range']::-moz-range-track { + width: 100%; + height: 9.5px; + -moz-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; +} + +input[type='range']::-moz-range-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']::-ms-track { + width: 100%; + height: 9.5px; + background: transparent; + border-color: transparent; + border-width: 16px 0; + color: transparent; +} + +input[type='range']::-ms-fill-lower { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-fill-upper { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; +} + +input[type='range']::-ms-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + border: 1px solid #000; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); +} + +input[type='range']:focus::-ms-fill-lower { + background: #161f27; + background: var(--background); +} + +input[type='range']:focus::-ms-fill-upper { + background: #161f27; + background: var(--background); +} + +a { + text-decoration: none; + color: #41adff; + color: var(--links); +} + +a:hover { + text-decoration: underline; +} + +code { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +samp { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +time { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; +} + +pre > code { + padding: 10px; + display: block; + overflow-x: auto; +} + +var { + color: #d941e2; + color: var(--variable); + font-style: normal; + font-family: monospace; +} + +kbd { + background: #161f27; + background: var(--background); + border: 1px solid #526980; + border: 1px solid var(--border); + border-radius: 2px; + color: #dbdbdb; + color: var(--text-main); + padding: 2px 4px 2px 4px; +} + +img, + video { + max-width: 100%; + height: auto; +} + +hr { + border: none; + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +table { + border-collapse: collapse; + margin-bottom: 10px; + width: 100%; + table-layout: fixed; +} + +table caption { + text-align: left; +} + +td, + th { + padding: 6px; + text-align: left; + vertical-align: top; + word-wrap: break-word; +} + +thead { + border-bottom: 1px solid #526980; + border-bottom: 1px solid var(--border); +} + +tfoot { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); +} + +tbody tr:nth-child(even) { + background-color: #161f27; + background-color: var(--background); +} + +tbody tr:nth-child(even) button { + background-color: #1a242f; + background-color: var(--background-alt); +} + +tbody tr:nth-child(even) button:hover { + background-color: #202b38; + background-color: var(--background-body); +} + +::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +::-webkit-scrollbar-track { + background: #161f27; + background: var(--background); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb { + background: #040a0f; + background: var(--scrollbar-thumb); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgb(0, 0, 0); + background: var(--scrollbar-thumb-hover); +} + +::-moz-selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +::selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); +} + +details { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #1a242f; + background-color: var(--background-alt); + padding: 10px 10px 0; + margin: 1em 0; + border-radius: 6px; + overflow: hidden; +} + +details[open] { + padding: 10px; +} + +details > :last-child { + margin-bottom: 0; +} + +details[open] summary { + margin-bottom: 10px; +} + +summary { + display: list-item; + background-color: #161f27; + background-color: var(--background); + padding: 10px; + margin: -10px -10px 0; + cursor: pointer; + outline: none; +} + +summary:hover, + summary:focus { + text-decoration: underline; +} + +details > :not(summary) { + margin-top: 0; +} + +summary::-webkit-details-marker { + color: #dbdbdb; + color: var(--text-main); +} + +dialog { + background-color: #1a242f; + background-color: var(--background-alt); + color: #dbdbdb; + color: var(--text-main); + border: none; + border-radius: 6px; + border-color: #526980; + border-color: var(--border); + padding: 10px 30px; +} + +dialog > header:first-child { + background-color: #161f27; + background-color: var(--background); + border-radius: 6px 6px 0 0; + margin: -10px -30px 10px; + padding: 10px; + text-align: center; +} + +dialog::backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); +} + +footer { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + padding-top: 10px; + color: #a9b1ba; + color: var(--text-muted); +} + +body > footer { + margin-top: 40px; +} + +@media print { + body, + pre, + code, + summary, + details, + button, + input, + textarea { + background-color: #fff; + } + + button, + input, + textarea { + border: 1px solid #000; + } + + body, + h1, + h2, + h3, + h4, + h5, + h6, + pre, + code, + button, + input, + textarea, + footer, + summary, + strong { + color: #000; + } + + summary::marker { + color: #000; + } + + summary::-webkit-details-marker { + color: #000; + } + + tbody tr:nth-child(even) { + background-color: #f2f2f2; + } + + a { + color: #00f; + text-decoration: underline; + } +} diff --git a/e2e/react/schema.graphql b/e2e/react/schema.graphql index 296f94979e..42c28bca36 100644 --- a/e2e/react/schema.graphql +++ b/e2e/react/schema.graphql @@ -40,8 +40,13 @@ scalar DateTime scalar File enum ForceReturn { + """Some error""" ERROR + + """Normal""" NORMAL + + """No value""" NULL } @@ -55,10 +60,15 @@ type Message1 { message: String! } +"""A monkey.""" type Monkey implements Animal & Node { + """Whether the monkey has a banana or not""" hasBanana: Boolean! id: ID! name: String! + + """Whether the monkey has a banana or not""" + oldHasBanana: Boolean @deprecated(reason: "Use hasBanana") } type MonkeyConnection implements AnimalConnection { @@ -76,7 +86,16 @@ type Mutation { addCity(name: String!): City! addLibrary(city: ID!, name: String!): Library! addNonNullUser(birthDate: DateTime!, delay: Int, enumValue: MyEnum, force: ForceReturn, name: String!, snapshot: String!, types: [TypeOfUser!]): User! - addUser(birthDate: DateTime!, delay: Int, enumValue: MyEnum, force: ForceReturn, name: String!, snapshot: String!, types: [TypeOfUser!]): User + addUser( + """The users birth date""" + birthDate: DateTime! + delay: Int + enumValue: MyEnum + force: ForceReturn + name: String! + snapshot: String! + types: [TypeOfUser!] + ): User createA(a: String!): A! createB(b: String!): B! deleteBook(book: ID!, delay: Int, force: ForceReturn): Book @@ -88,11 +107,16 @@ type Mutation { updateUser(birthDate: DateTime, delay: Int, id: ID!, name: String, snapshot: String!): User! } +"""Can be Value1 or Value2.""" enum MyEnum { + """The first value""" Value1 - Value2 + + """The second value""" + Value2 @deprecated(reason: "Use Value1 instead") } +"""A node.""" interface Node { id: ID! } @@ -111,6 +135,8 @@ type Query { cities: [City]! city(delay: Int, id: ID!): City hello: String + + """Get a monkey by its id""" monkey(id: ID!): Monkey monkeys: MonkeyConnection! node(id: ID!): Node diff --git a/e2e/react/src/+index.jsx b/e2e/react/src/+index.jsx index 5b75493abd..0af98d7924 100644 --- a/e2e/react/src/+index.jsx +++ b/e2e/react/src/+index.jsx @@ -7,10 +7,7 @@ export default function App({ children }) { Houdini • e2e • React - + {children} diff --git a/e2e/react/src/routes/+layout.tsx b/e2e/react/src/routes/+layout.tsx index e9bd1e29e1..0c322c8faf 100644 --- a/e2e/react/src/routes/+layout.tsx +++ b/e2e/react/src/routes/+layout.tsx @@ -3,7 +3,6 @@ import React from 'react' import { routes } from '~/utils/routes' import type { LayoutProps } from './$types' -import './index.css' export default function ({ children }: LayoutProps) { // save the cache reference on the window @@ -13,22 +12,24 @@ export default function ({ children }: LayoutProps) { // @ts-ignore globalThis.window.cache = cache } - }) + }, []) return ( <> - -
+
{children}
) diff --git a/e2e/react/src/routes/component_fields/arguments/test.ts b/e2e/react/src/routes/component_fields/arguments/test.ts index 4e2a41d9c0..3ceccc5886 100644 --- a/e2e/react/src/routes/component_fields/arguments/test.ts +++ b/e2e/react/src/routes/component_fields/arguments/test.ts @@ -9,7 +9,6 @@ test('Component fields with correct argument value', async ({ page }) => { const images = await page.locator('img').all() // every image should have a size of 50 for (const image of images) { - expect(await image.getAttribute('height')).toBe('50') - expect(await image.getAttribute('src')).toContain('size=50') + expect(await image.getAttribute('src')).toContain('size=100') } }) diff --git a/e2e/react/src/routes/handle/[userID]/+page.gql b/e2e/react/src/routes/handle/[userID]/+page.gql new file mode 100644 index 0000000000..5004544662 --- /dev/null +++ b/e2e/react/src/routes/handle/[userID]/+page.gql @@ -0,0 +1,5 @@ +query HandleUserQuery($userID: ID!, $size: Int) { + user(id: $userID, snapshot: "route_params") { + avatarURL(size: $size) + } +} diff --git a/e2e/react/src/routes/handle/[userID]/+page.tsx b/e2e/react/src/routes/handle/[userID]/+page.tsx new file mode 100644 index 0000000000..cdbf96162c --- /dev/null +++ b/e2e/react/src/routes/handle/[userID]/+page.tsx @@ -0,0 +1,45 @@ +import { PageProps } from './$types' + +export default function HandleTest({ HandleUserQuery, HandleUserQuery$handle }: PageProps) { + // grab the current size from the query handle + const currentSize = HandleUserQuery$handle.variables.size ?? 50 + + return ( +
+
+ + + +
+
+ {HandleUserQuery.user.avatarURL} +
+
{JSON.stringify(HandleUserQuery$handle.variables)}
+
+ ) +} diff --git a/e2e/react/src/routes/handle/test.ts b/e2e/react/src/routes/handle/test.ts new file mode 100644 index 0000000000..4fcf59997d --- /dev/null +++ b/e2e/react/src/routes/handle/test.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test' +import { dataUsers } from 'e2e-api/graphql.mjs' +import { routes } from '~/utils/routes' +import { goto } from '~/utils/testsHelper.js' + +test('handle fetch remembers server-side variables', async function ({ page }) { + // in this test, variables are stringified in the #variables div + const getVariables = async () => { + return JSON.parse((await page.textContent('#variables')) as string) + } + + // visit the page for user 2 + await goto(page, routes.handle_2) + await expect(page.textContent('#result')).resolves.toEqual(dataUsers[1].avatarURL) + + // check the variables + await expect(getVariables()).resolves.toEqual({ userID: '2' }) + + // load the larger image and wait for it to resolve + await page.click('#larger') + await page.waitForSelector('[data-size="51"]', { + timeout: 1000, + }) + + // make sure the and variables line up + await expect(page.textContent('#result')).resolves.toContain(dataUsers[1].avatarURL) + await expect(getVariables()).resolves.toEqual({ userID: '2', size: 51 }) + + // now load user 1 + await page.click('#user-1') + await page.waitForSelector('[data-user="1"]', { + timeout: 1000, + }) + + // make sure the and variables line up + await expect(page.textContent('#result')).resolves.toContain(dataUsers[0].avatarURL) + await expect(getVariables()).resolves.toEqual({ userID: '1', size: 51 }) +}) + +test('handle survives navigation', async function ({ page }) { + // in this test, variables are stringified in the #variables div + const getVariables = async () => { + return JSON.parse((await page.textContent('#variables')) as string) + } + + // visit the page for user 2 + await goto(page, routes.handle_2) + await expect(page.textContent('#result')).resolves.toEqual(dataUsers[1].avatarURL) + await expect(getVariables()).resolves.toEqual({ userID: '2' }) + + // navigate to the page for user 1 by clicking on the nav link + await page.click(`a[href="${routes.handle_1}"]`) + // wait for the page to load + await page.waitForSelector('[data-user="1"]', { + timeout: 1000, + }) + await expect(page.textContent('#result')).resolves.toEqual(dataUsers[0].avatarURL) + await expect(getVariables()).resolves.toEqual({ userID: '1' }) + + // click on the larger button and wait for it to resolve + await page.click('#larger') + await page.waitForSelector('[data-size="51"]', { + timeout: 1000, + }) + + // make sure the and variables line up + await expect(page.textContent('#result')).resolves.toContain(dataUsers[0].avatarURL) + await expect(getVariables()).resolves.toEqual({ userID: '1', size: 51 }) +}) diff --git a/e2e/react/src/routes/hello-world/+page.tsx b/e2e/react/src/routes/hello-world/+page.tsx index 601f66f7ee..c1d6c3dccc 100644 --- a/e2e/react/src/routes/hello-world/+page.tsx +++ b/e2e/react/src/routes/hello-world/+page.tsx @@ -1,5 +1,5 @@ import type { PageProps } from './$types' export default function ({ HelloWorld }: PageProps) { - return
{HelloWorld.hello}
+ return
{HelloWorld.hello}
} diff --git a/e2e/react/src/routes/hello-world/test.ts b/e2e/react/src/routes/hello-world/test.ts new file mode 100644 index 0000000000..1be1dc80d2 --- /dev/null +++ b/e2e/react/src/routes/hello-world/test.ts @@ -0,0 +1,9 @@ +import { expect, test } from '@playwright/test' +import { routes } from '~/utils/routes' +import { goto } from '~/utils/testsHelper.js' + +test('Hello World', async ({ page }) => { + await goto(page, routes.hello) + + await expect(page.textContent('#result')).resolves.toEqual('Hello World! // From Houdini!') +}) diff --git a/e2e/react/src/routes/index.css b/e2e/react/src/routes/index.css deleted file mode 100644 index b5c61c9567..0000000000 --- a/e2e/react/src/routes/index.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/e2e/react/src/routes/pagination/query/connection-backwards/+page.gql b/e2e/react/src/routes/pagination/query/connection-backwards/+page.gql new file mode 100644 index 0000000000..1fb2738826 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-backwards/+page.gql @@ -0,0 +1,9 @@ +query BackwardsCursorPaginationQuery { + usersConnection(last: 2, snapshot: "pagination-query-backwards-cursor") @paginate { + edges { + node { + name + } + } + } +} diff --git a/e2e/react/src/routes/pagination/query/connection-backwards/+page.tsx b/e2e/react/src/routes/pagination/query/connection-backwards/+page.tsx new file mode 100644 index 0000000000..f2acd5f391 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-backwards/+page.tsx @@ -0,0 +1,30 @@ +import { CachePolicy } from '$houdini' + +import type { PageProps } from './$types' + +export default function ({ + BackwardsCursorPaginationQuery, + BackwardsCursorPaginationQuery$handle, +}: PageProps) { + const handle = BackwardsCursorPaginationQuery$handle + + return ( + <> +
+ {BackwardsCursorPaginationQuery.usersConnection.edges + .map(({ node }) => node?.name) + .join(', ')} +
+ +
{JSON.stringify(handle.pageInfo)}
+ + + + + + ) +} diff --git a/e2e/react/src/routes/pagination/query/connection-backwards/test.ts b/e2e/react/src/routes/pagination/query/connection-backwards/test.ts new file mode 100644 index 0000000000..0e5d4faba0 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-backwards/test.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test' +import { routes } from '~/utils/routes.js' +import { + expect_1_gql, + expect_0_gql, + expect_to_be, + expectToContain, + goto, +} from '~/utils/testsHelper.js' + +test.describe('backwards cursor paginatedQuery', () => { + test('loadPreviousPage', async ({ page }) => { + await goto(page, routes.pagination_query_backwards) + + await expect_to_be(page, 'Eddie Murphy, Clint Eastwood') + + // wait for the api response + await expect_1_gql(page, 'button[id=previous]') + + // make sure we got the new content + await expect_to_be(page, 'Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood') + }) + + test('refetch', async ({ page }) => { + await goto(page, routes.pagination_query_backwards) + + // wait for the api response + await expect_1_gql(page, 'button[id=previous]') + + // wait for the api response + const response = await expect_1_gql(page, 'button[id=refetch]') + await expect_to_be(page, 'Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood') + }) + + test('page info tracks connection state', async ({ page }) => { + await goto(page, routes.pagination_query_backwards) + + const data = [ + 'Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood', + 'Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood', + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood', + ] + + // load the previous 3 pages + for (let i = 0; i < 3; i++) { + // wait for the request to resolve + await expect_1_gql(page, 'button[id=previous]') + // check the page info + await expect_to_be(page, data[i]) + } + + // make sure we have all of the data loaded + await expect_to_be(page, data[2]) + + await expectToContain(page, `"hasPreviousPage":false`) + + await expect_0_gql(page, 'button[id=previous]') + }) +}) diff --git a/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.gql b/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.gql new file mode 100644 index 0000000000..e1213ac020 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.gql @@ -0,0 +1,13 @@ +query BidirectionalPaginationQuery { + usersConnection( + after: "YXJyYXljb25uZWN0aW9uOjE=" + first: 2 + snapshot: "pagination-query-bdiriectional-cursor" + ) @paginate { + edges { + node { + name + } + } + } +} diff --git a/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.svelte b/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.svelte new file mode 100644 index 0000000000..bc919f58d2 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.svelte @@ -0,0 +1,34 @@ + + +
+ {$result.data?.usersConnection.edges.map(({ node }) => node?.name).join(', ')} +
+ +
+ {JSON.stringify($result.pageInfo)} +
+ + + + + diff --git a/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.tsx b/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.tsx new file mode 100644 index 0000000000..90ccd9235d --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-bidirectional/+page.tsx @@ -0,0 +1,34 @@ +import { CachePolicy } from '$houdini' + +import type { PageProps } from './$types' + +export default function ({ + BidirectionalPaginationQuery, + BidirectionalPaginationQuery$handle, +}: PageProps) { + const handle = BidirectionalPaginationQuery$handle + + return ( + <> +
+ {BidirectionalPaginationQuery.usersConnection.edges + .map(({ node }) => node?.name) + .join(', ')} +
+ +
{JSON.stringify(handle.pageInfo)}
+ + + + + + + + ) +} diff --git a/e2e/react/src/routes/pagination/query/connection-bidirectional/spec.ts b/e2e/react/src/routes/pagination/query/connection-bidirectional/spec.ts new file mode 100644 index 0000000000..cf4fbaf38b --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-bidirectional/spec.ts @@ -0,0 +1,107 @@ +import { test } from '@playwright/test' +import { routes } from '~/utils/routes.js' +import { expect_to_be, expectToContain, expect_1_gql, goto } from '~/utils/testsHelper.js' + +test.describe('bidirectional cursor paginated query', () => { + test('backwards and then forwards', async ({ page }) => { + await goto(page, routes.pagination_query_bidirectional) + + await expect_to_be(page, 'Morgan Freeman, Tom Hanks') + + /// Click on the previous button + + // load the previous page and wait for the response + await expect_1_gql(page, 'button[id=previous]') + + // make sure we got the new content + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + + // there should be a next page + await expectToContain(page, `"hasNextPage":true`) + // there should be no previous page + await expectToContain(page, `"hasPreviousPage":false`) + + /// Click on the next button + + // load the next page and wait for the response + await expect_1_gql(page, 'button[id=next]') + + // there should be no previous page + await expectToContain(page, `"hasPreviousPage":false`) + // there should be a next page + await expectToContain(page, `"hasNextPage":true`) + + // make sure we got the new content + await expect_to_be( + page, + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford' + ) + + /// Click on the next button + + // load the next page and wait for the response + await expect_1_gql(page, 'button[id=next]') + + // there should be no previous page + await expectToContain(page, `"hasPreviousPage":false`) + // there should be a next page + await expectToContain(page, `"hasNextPage":false`) + + // make sure we got the new content + await expect_to_be( + page, + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood' + ) + }) + + test('forwards then backwards and then forwards again', async ({ page }) => { + await goto(page, routes.pagination_query_bidirectional) + + await expect_to_be(page, 'Morgan Freeman, Tom Hanks') + + /// Click on the next button + + // load the next page and wait for the response + await expect_1_gql(page, 'button[id=next]') + + // there should be no previous page + await expectToContain(page, `"hasPreviousPage":true`) + // there should be a next page + await expectToContain(page, `"hasNextPage":true`) + + // make sure we got the new content + await expect_to_be(page, 'Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford') + + /// Click on the previous button + + // load the previous page and wait for the response + await expect_1_gql(page, 'button[id=previous]') + + // make sure we got the new content + await expect_to_be( + page, + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford' + ) + + // there should be a next page + await expectToContain(page, `"hasNextPage":true`) + // there should be no previous page + await expectToContain(page, `"hasPreviousPage":false`) + + /// Click on the next button + + // load the next page and wait for the response + await expect_1_gql(page, 'button[id=next]') + + // there should be no previous page + await expectToContain(page, `"hasPreviousPage":false`) + // there should be a next page + await expectToContain(page, `"hasNextPage":false`) + + // make sure we got the new content + await expect_to_be( + page, + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood' + ) + }) +}) diff --git a/e2e/react/src/routes/pagination/query/connection-forwards/+page.gql b/e2e/react/src/routes/pagination/query/connection-forwards/+page.gql new file mode 100644 index 0000000000..a4d8953f3c --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-forwards/+page.gql @@ -0,0 +1,9 @@ +query ForwardsCursorPaginationQuery { + usersConnection(first: 2, snapshot: "pagination-query-forwards-cursor") @paginate { + edges { + node { + name + } + } + } +} diff --git a/e2e/react/src/routes/pagination/query/connection-forwards/+page.tsx b/e2e/react/src/routes/pagination/query/connection-forwards/+page.tsx new file mode 100644 index 0000000000..ca47082b81 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-forwards/+page.tsx @@ -0,0 +1,30 @@ +import { CachePolicy } from '$houdini' + +import type { PageProps } from './$types' + +export default function ({ + ForwardsCursorPaginationQuery, + ForwardsCursorPaginationQuery$handle, +}: PageProps) { + const handle = ForwardsCursorPaginationQuery$handle + + return ( + <> +
+ {ForwardsCursorPaginationQuery.usersConnection.edges + .map(({ node }) => node?.name) + .join(', ')} +
+ +
{JSON.stringify(handle.pageInfo)}
+ + + + + + ) +} diff --git a/e2e/react/src/routes/pagination/query/connection-forwards/test.ts b/e2e/react/src/routes/pagination/query/connection-forwards/test.ts new file mode 100644 index 0000000000..944182bde4 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/connection-forwards/test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test' +import { routes } from '~/utils/routes.js' +import { + expect_1_gql, + expect_0_gql, + expect_to_be, + expectToContain, + goto, +} from '~/utils/testsHelper.js' + +test.describe('forwards cursor paginatedQuery', () => { + test('loadNextPage', async ({ page }) => { + await goto(page, routes.pagination_query_forwards) + + await expect_to_be(page, 'Bruce Willis, Samuel Jackson') + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // make sure we got the new content + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + }) + + test('refetch', async ({ page }) => { + await goto(page, routes.pagination_query_forwards) + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // wait for the api response + await expect_1_gql(page, 'button[id=refetch]') + // make sure we got the new content + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + }) + + test('page info tracks connection state', async ({ page }) => { + await goto(page, routes.pagination_query_forwards) + + const data = [ + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks', + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford', + 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford, Eddie Murphy, Clint Eastwood', + ] + + // load the next 3 pages + for (let i = 0; i < 3; i++) { + // wait for the request to resolve + await expect_1_gql(page, 'button[id=next]') + // check the page info + await expect_to_be(page, data[i]) + } + + // make sure we have all of the data loaded + await expect_to_be(page, data[2]) + + await expectToContain(page, `"hasNextPage":false`) + + await expect_0_gql(page, 'button[id=next]') + }) +}) diff --git a/e2e/react/src/routes/pagination/query/offset-singlepage/+page.gql b/e2e/react/src/routes/pagination/query/offset-singlepage/+page.gql new file mode 100644 index 0000000000..a3fffd54b5 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset-singlepage/+page.gql @@ -0,0 +1,6 @@ +query OffsetPaginationSinglePageQuery { + usersList(limit: 2, snapshot: "pagination-query-offset-single-page") + @paginate(mode: SinglePage) { + name + } +} diff --git a/e2e/react/src/routes/pagination/query/offset-singlepage/+page.tsx b/e2e/react/src/routes/pagination/query/offset-singlepage/+page.tsx new file mode 100644 index 0000000000..7371afe2d4 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset-singlepage/+page.tsx @@ -0,0 +1,26 @@ +import { CachePolicy } from '$houdini' + +import type { PageProps } from './$types' + +export default function ({ + OffsetPaginationSinglePageQuery, + OffsetPaginationSinglePageQuery$handle, +}: PageProps) { + const handle = OffsetPaginationSinglePageQuery$handle + + return ( + <> +
+ {OffsetPaginationSinglePageQuery.usersList.map((user) => user?.name).join(', ')} +
+ + + + + + ) +} diff --git a/e2e/react/src/routes/pagination/query/offset-singlepage/test.ts b/e2e/react/src/routes/pagination/query/offset-singlepage/test.ts new file mode 100644 index 0000000000..a6cfe419f4 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset-singlepage/test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test' +import { routes } from '~/utils/routes.js' +import { expect_1_gql, expect_to_be, goto } from '~/utils/testsHelper.js' + +test.describe('offset paginatedQuery', () => { + test('loadNextPage', async ({ page }) => { + await goto(page, routes.pagination_query_offset_singlepage) + + await expect_to_be(page, 'Bruce Willis, Samuel Jackson') + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // make sure we got the new page + await expect_to_be(page, 'Morgan Freeman, Tom Hanks') + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // make sure we got the new page + await expect_to_be(page, 'Will Smith, Harrison Ford') + }) +}) diff --git a/e2e/react/src/routes/pagination/query/offset-variable/[limit]/+page.gql b/e2e/react/src/routes/pagination/query/offset-variable/[limit]/+page.gql new file mode 100644 index 0000000000..7286c4e045 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset-variable/[limit]/+page.gql @@ -0,0 +1,5 @@ +query OffsetVariablePaginationQuery($limit: Int!) { + usersList(limit: $limit, snapshot: "pagination-query-offset-variables") @paginate { + name + } +} diff --git a/e2e/react/src/routes/pagination/query/offset-variable/[limit]/+page.tsx b/e2e/react/src/routes/pagination/query/offset-variable/[limit]/+page.tsx new file mode 100644 index 0000000000..19413d1618 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset-variable/[limit]/+page.tsx @@ -0,0 +1,26 @@ +import { CachePolicy } from '$houdini' + +import type { PageProps } from './$types' + +export default function ({ + OffsetVariablePaginationQuery, + OffsetVariablePaginationQuery$handle, +}: PageProps) { + const handle = OffsetVariablePaginationQuery$handle + + return ( + <> +
+ {OffsetVariablePaginationQuery.usersList.map((user) => user?.name).join(', ')} +
+ + + + + + ) +} diff --git a/e2e/react/src/routes/pagination/query/offset-variable/[limit]/test.ts b/e2e/react/src/routes/pagination/query/offset-variable/[limit]/test.ts new file mode 100644 index 0000000000..cf4b987d71 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset-variable/[limit]/test.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test' +import { routes } from '~/utils/routes.js' +import { expect_1_gql, expect_to_be, goto } from '~/utils/testsHelper.js' + +test.describe('offset paginatedQuery', () => { + test('loadNextPage', async ({ page }) => { + await goto(page, routes.pagination_query_offset_variable) + + await expect_to_be(page, 'Bruce Willis, Samuel Jackson') + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // make sure we got the new content + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + }) + + test('refetch', async ({ page }) => { + await goto(page, routes.pagination_query_offset_variable) + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // wait for the api response + await expect_1_gql(page, 'button[id=refetch]') + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + }) +}) diff --git a/e2e/react/src/routes/pagination/query/offset/+page.gql b/e2e/react/src/routes/pagination/query/offset/+page.gql new file mode 100644 index 0000000000..0fc7963cba --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset/+page.gql @@ -0,0 +1,5 @@ +query OffsetPaginationQuery { + usersList(limit: 2, snapshot: "pagination-query-offset") @paginate { + name + } +} diff --git a/e2e/react/src/routes/pagination/query/offset/+page.tsx b/e2e/react/src/routes/pagination/query/offset/+page.tsx new file mode 100644 index 0000000000..e44eaf45c6 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset/+page.tsx @@ -0,0 +1,23 @@ +import { CachePolicy } from '$houdini' + +import type { PageProps } from './$types' + +export default function ({ OffsetPaginationQuery, OffsetPaginationQuery$handle }: PageProps) { + const handle = OffsetPaginationQuery$handle + + return ( + <> +
+ {OffsetPaginationQuery.usersList.map((user) => user?.name).join(', ')} +
+ + + + + + ) +} diff --git a/e2e/react/src/routes/pagination/query/offset/test.ts b/e2e/react/src/routes/pagination/query/offset/test.ts new file mode 100644 index 0000000000..eef83a7f48 --- /dev/null +++ b/e2e/react/src/routes/pagination/query/offset/test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test' +import { routes } from '~/utils/routes.js' +import { expect_1_gql, expect_to_be, goto } from '~/utils/testsHelper.js' + +test.describe('offset paginatedQuery', () => { + test('loadNextPage', async ({ page }) => { + await goto(page, routes.pagination_query_offset) + + await expect_to_be(page, 'Bruce Willis, Samuel Jackson') + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // make sure we got the new content + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + }) + + test('refetch', async ({ page }) => { + await goto(page, routes.pagination_query_offset) + + // wait for the api response + await expect_1_gql(page, 'button[id=next]') + + // wait for the api response + await expect_1_gql(page, 'button[id=refetch]') + // make sure we got the new content + await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks') + }) +}) diff --git a/e2e/react/src/routes/route_params/[id]/+page.tsx b/e2e/react/src/routes/route_params/[id]/+page.tsx index 528b326e7b..72a7ca4263 100644 --- a/e2e/react/src/routes/route_params/[id]/+page.tsx +++ b/e2e/react/src/routes/route_params/[id]/+page.tsx @@ -8,7 +8,7 @@ export default function ({ RouteParamsUserInfo }: PageProps) { const { user } = RouteParamsUserInfo return (
-
+
{route.params.id}: {user.name}
diff --git a/e2e/react/src/routes/route_params/test.ts b/e2e/react/src/routes/route_params/test.ts index 828a744a8c..d08bdd80b9 100644 --- a/e2e/react/src/routes/route_params/test.ts +++ b/e2e/react/src/routes/route_params/test.ts @@ -10,7 +10,7 @@ test('Component fields with correct argument value', async ({ page }) => { await expect_to_be(page, '1:Bruce Willis') // click on the link 2 - await page.click('user-link-2') + await page.click('#user-link-2') // wait some time await sleep(100) diff --git a/e2e/react/src/routes/scalars/test.ts b/e2e/react/src/routes/scalars/test.ts index 88107f81cc..bdd68bad3b 100644 --- a/e2e/react/src/routes/scalars/test.ts +++ b/e2e/react/src/routes/scalars/test.ts @@ -5,5 +5,7 @@ import { goto } from '~/utils/testsHelper.js' test('Scalars', async ({ page }) => { await goto(page, routes.scalars) - expect(page.textContent('#result')).toMatchSnapshot() + await expect(page.textContent('#result')).resolves.toEqual( + 'Bruce Willis-3/18/1955Samuel Jackson-12/20/1948Morgan Freeman-5/30/1937Tom Hanks-7/8/1956' + ) }) diff --git a/e2e/react/src/routes/spec.ts b/e2e/react/src/routes/spec.ts deleted file mode 100644 index 3c11b4cd0d..0000000000 --- a/e2e/react/src/routes/spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '@playwright/test' - -import { expect_to_be } from '../utils/testsHelper.js' - -test('Integration has the right title, we can start 🚀', async ({ page }) => { - await page.goto('/') - - await expect_to_be(page, "Houdini's React Interation tests", 'h1') -}) diff --git a/e2e/react/src/styles.css b/e2e/react/src/styles.css new file mode 100644 index 0000000000..61028c43c1 --- /dev/null +++ b/e2e/react/src/styles.css @@ -0,0 +1,890 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/** + * Forced dark theme version + */ + + :root { + --background-body: #202b38; + --background: #161f27; + --background-alt: #1a242f; + --selection: #1c76c5; + --text-main: #dbdbdb; + --text-bright: #fff; + --text-muted: #a9b1ba; + --links: #41adff; + --focus: #0096bfab; + --border: #526980; + --code: #ffbe85; + --animation-duration: 0.1s; + --button-base: #0c151c; + --button-hover: #040a0f; + --scrollbar-thumb: var(--button-hover); + --scrollbar-thumb-hover: rgb(0, 0, 0); + --form-placeholder: #a9a9a9; + --form-text: #fff; + --variable: #d941e2; + --highlight: #efdb43; + --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E"); + } + + html { + scrollbar-color: #040a0f #202b38; + scrollbar-color: var(--scrollbar-thumb) var(--background-body); + scrollbar-width: thin; + } + + body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; + line-height: 1.4; + max-width: 800px; + margin: 20px auto; + padding: 0 10px; + word-wrap: break-word; + color: #dbdbdb; + color: var(--text-main); + background: #202b38; + background: var(--background-body); + text-rendering: optimizeLegibility; + } + + button { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; + } + + input { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; + } + + textarea { + transition: + background-color 0.1s linear, + border-color 0.1s linear, + color 0.1s linear, + box-shadow 0.1s linear, + transform 0.1s ease; + transition: + background-color var(--animation-duration) linear, + border-color var(--animation-duration) linear, + color var(--animation-duration) linear, + box-shadow var(--animation-duration) linear, + transform var(--animation-duration) ease; + } + + h1 { + font-size: 2.2em; + margin-top: 0; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-bottom: 12px; + margin-top: 24px; + } + + h1 { + color: #fff; + color: var(--text-bright); + } + + h2 { + color: #fff; + color: var(--text-bright); + } + + h3 { + color: #fff; + color: var(--text-bright); + } + + h4 { + color: #fff; + color: var(--text-bright); + } + + h5 { + color: #fff; + color: var(--text-bright); + } + + h6 { + color: #fff; + color: var(--text-bright); + } + + strong { + color: #fff; + color: var(--text-bright); + } + + h1, + h2, + h3, + h4, + h5, + h6, + b, + strong, + th { + font-weight: 600; + } + + q::before { + content: none; + } + + q::after { + content: none; + } + + blockquote { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; + } + + q { + border-left: 4px solid #0096bfab; + border-left: 4px solid var(--focus); + margin: 1.5em 0; + padding: 0.5em 1em; + font-style: italic; + } + + blockquote > footer { + font-style: normal; + border: 0; + } + + blockquote cite { + font-style: normal; + } + + address { + font-style: normal; + } + + a[href^='mailto\:']::before { + content: '📧 '; + } + + a[href^='tel\:']::before { + content: '📞 '; + } + + a[href^='sms\:']::before { + content: '💬 '; + } + + mark { + background-color: #efdb43; + background-color: var(--highlight); + border-radius: 2px; + padding: 0 2px 0 2px; + color: #000; + } + + a > code, + a > strong { + color: inherit; + } + + button, + select, + input[type='submit'], + input[type='reset'], + input[type='button'], + input[type='checkbox'], + input[type='range'], + input[type='radio'] { + cursor: pointer; + } + + input, + select { + display: block; + } + + [type='checkbox'], + [type='radio'] { + display: initial; + } + + input { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; + } + + button { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; + } + + textarea { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; + } + + select { + color: #fff; + color: var(--form-text); + background-color: #161f27; + background-color: var(--background); + font-family: inherit; + font-size: inherit; + margin-right: 6px; + margin-bottom: 6px; + padding: 10px; + border: none; + border-radius: 6px; + outline: none; + } + + button { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; + } + + input[type='submit'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; + } + + input[type='reset'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; + } + + input[type='button'] { + background-color: #0c151c; + background-color: var(--button-base); + padding-right: 30px; + padding-left: 30px; + } + + button:hover { + background: #040a0f; + background: var(--button-hover); + } + + input[type='submit']:hover { + background: #040a0f; + background: var(--button-hover); + } + + input[type='reset']:hover { + background: #040a0f; + background: var(--button-hover); + } + + input[type='button']:hover { + background: #040a0f; + background: var(--button-hover); + } + + input[type='color'] { + min-height: 2rem; + padding: 8px; + cursor: pointer; + } + + input[type='checkbox'], + input[type='radio'] { + height: 1em; + width: 1em; + } + + input[type='radio'] { + border-radius: 100%; + } + + input { + vertical-align: top; + } + + label { + vertical-align: middle; + margin-bottom: 4px; + display: inline-block; + } + + input:not([type='checkbox']):not([type='radio']), + input[type='range'], + select, + button, + textarea { + -webkit-appearance: none; + } + + textarea { + display: block; + margin-right: 0; + box-sizing: border-box; + resize: vertical; + } + + textarea:not([cols]) { + width: 100%; + } + + textarea:not([rows]) { + min-height: 40px; + height: 140px; + } + + select { + background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; + background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; + padding-right: 35px; + } + + select::-ms-expand { + display: none; + } + + select[multiple] { + padding-right: 10px; + background-image: none; + overflow-y: auto; + } + + input:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); + } + + select:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); + } + + button:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); + } + + textarea:focus { + box-shadow: 0 0 0 2px #0096bfab; + box-shadow: 0 0 0 2px var(--focus); + } + + input[type='checkbox']:active, + input[type='radio']:active, + input[type='submit']:active, + input[type='reset']:active, + input[type='button']:active, + input[type='range']:active, + button:active { + transform: translateY(2px); + } + + input:disabled, + select:disabled, + button:disabled, + textarea:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + ::-moz-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); + } + + :-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); + } + + ::-ms-input-placeholder { + color: #a9a9a9; + color: var(--form-placeholder); + } + + ::placeholder { + color: #a9a9a9; + color: var(--form-placeholder); + } + + fieldset { + border: 1px #0096bfab solid; + border: 1px var(--focus) solid; + border-radius: 6px; + margin: 0; + margin-bottom: 12px; + padding: 10px; + } + + legend { + font-size: 0.9em; + font-weight: 600; + } + + input[type='range'] { + margin: 10px 0; + padding: 10px 0; + background: transparent; + } + + input[type='range']:focus { + outline: none; + } + + input[type='range']::-webkit-slider-runnable-track { + width: 100%; + height: 9.5px; + -webkit-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; + } + + input[type='range']::-webkit-slider-thumb { + box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + -webkit-appearance: none; + margin-top: -7px; + } + + input[type='range']:focus::-webkit-slider-runnable-track { + background: #161f27; + background: var(--background); + } + + input[type='range']::-moz-range-track { + width: 100%; + height: 9.5px; + -moz-transition: 0.2s; + transition: 0.2s; + background: #161f27; + background: var(--background); + border-radius: 3px; + } + + input[type='range']::-moz-range-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + } + + input[type='range']::-ms-track { + width: 100%; + height: 9.5px; + background: transparent; + border-color: transparent; + border-width: 16px 0; + color: transparent; + } + + input[type='range']::-ms-fill-lower { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + } + + input[type='range']::-ms-fill-upper { + background: #161f27; + background: var(--background); + border: 0.2px solid #010101; + border-radius: 3px; + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + } + + input[type='range']::-ms-thumb { + box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; + border: 1px solid #000; + height: 20px; + width: 20px; + border-radius: 50%; + background: #526980; + background: var(--border); + } + + input[type='range']:focus::-ms-fill-lower { + background: #161f27; + background: var(--background); + } + + input[type='range']:focus::-ms-fill-upper { + background: #161f27; + background: var(--background); + } + + a { + text-decoration: none; + color: #41adff; + color: var(--links); + } + + a:hover { + text-decoration: underline; + } + + code { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; + } + + samp { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; + } + + time { + background: #161f27; + background: var(--background); + color: #ffbe85; + color: var(--code); + padding: 2.5px 5px; + border-radius: 6px; + font-size: 1em; + } + + pre > code { + padding: 10px; + display: block; + overflow-x: auto; + } + + var { + color: #d941e2; + color: var(--variable); + font-style: normal; + font-family: monospace; + } + + kbd { + background: #161f27; + background: var(--background); + border: 1px solid #526980; + border: 1px solid var(--border); + border-radius: 2px; + color: #dbdbdb; + color: var(--text-main); + padding: 2px 4px 2px 4px; + } + + img, + video { + max-width: 100%; + height: auto; + } + + hr { + border: none; + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + } + + table { + border-collapse: collapse; + margin-bottom: 10px; + width: 100%; + table-layout: fixed; + } + + table caption { + text-align: left; + } + + td, + th { + padding: 6px; + text-align: left; + vertical-align: top; + word-wrap: break-word; + } + + thead { + border-bottom: 1px solid #526980; + border-bottom: 1px solid var(--border); + } + + tfoot { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + } + + tbody tr:nth-child(even) { + background-color: #161f27; + background-color: var(--background); + } + + tbody tr:nth-child(even) button { + background-color: #1a242f; + background-color: var(--background-alt); + } + + tbody tr:nth-child(even) button:hover { + background-color: #202b38; + background-color: var(--background-body); + } + + ::-webkit-scrollbar { + height: 10px; + width: 10px; + } + + ::-webkit-scrollbar-track { + background: #161f27; + background: var(--background); + border-radius: 6px; + } + + ::-webkit-scrollbar-thumb { + background: #040a0f; + background: var(--scrollbar-thumb); + border-radius: 6px; + } + + ::-webkit-scrollbar-thumb:hover { + background: rgb(0, 0, 0); + background: var(--scrollbar-thumb-hover); + } + + ::-moz-selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); + } + + ::selection { + background-color: #1c76c5; + background-color: var(--selection); + color: #fff; + color: var(--text-bright); + } + + details { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #1a242f; + background-color: var(--background-alt); + padding: 10px 10px 0; + margin: 1em 0; + border-radius: 6px; + overflow: hidden; + } + + details[open] { + padding: 10px; + } + + details > :last-child { + margin-bottom: 0; + } + + details[open] summary { + margin-bottom: 10px; + } + + summary { + display: list-item; + background-color: #161f27; + background-color: var(--background); + padding: 10px; + margin: -10px -10px 0; + cursor: pointer; + outline: none; + } + + summary:hover, + summary:focus { + text-decoration: underline; + } + + details > :not(summary) { + margin-top: 0; + } + + summary::-webkit-details-marker { + color: #dbdbdb; + color: var(--text-main); + } + + dialog { + background-color: #1a242f; + background-color: var(--background-alt); + color: #dbdbdb; + color: var(--text-main); + border: none; + border-radius: 6px; + border-color: #526980; + border-color: var(--border); + padding: 10px 30px; + } + + dialog > header:first-child { + background-color: #161f27; + background-color: var(--background); + border-radius: 6px 6px 0 0; + margin: -10px -30px 10px; + padding: 10px; + text-align: center; + } + + dialog::-webkit-backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + } + + dialog::backdrop { + background: #0000009c; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + } + + footer { + border-top: 1px solid #526980; + border-top: 1px solid var(--border); + padding-top: 10px; + color: #a9b1ba; + color: var(--text-muted); + } + + body > footer { + margin-top: 40px; + } + + @media print { + body, + pre, + code, + summary, + details, + button, + input, + textarea { + background-color: #fff; + } + + button, + input, + textarea { + border: 1px solid #000; + } + + body, + h1, + h2, + h3, + h4, + h5, + h6, + pre, + code, + button, + input, + textarea, + footer, + summary, + strong { + color: #000; + } + + summary::marker { + color: #000; + } + + summary::-webkit-details-marker { + color: #000; + } + + tbody tr:nth-child(even) { + background-color: #f2f2f2; + } + + a { + color: #00f; + text-decoration: underline; + } + } diff --git a/e2e/react/src/utils/routes.ts b/e2e/react/src/utils/routes.ts index 9d1040ab72..481cbbcf1f 100644 --- a/e2e/react/src/utils/routes.ts +++ b/e2e/react/src/utils/routes.ts @@ -1,7 +1,15 @@ export const routes = { hello: '/hello-world', - scalars: 'scalars', + scalars: '/scalars', componentFields_simple: '/component_fields/simple', componentFields_arguments: '/component_fields/arguments', route_params: '/route_params/1', -} + handle_1: '/handle/1', + handle_2: '/handle/2', + pagination_query_backwards: '/pagination/query/connection-backwards', + pagination_query_forwards: '/pagination/query/connection-forwards', + pagination_query_bidirectional: '/pagination/query/connection-bidirectional', + pagination_query_offset: '/pagination/query/offset', + pagination_query_offset_singlepage: '/pagination/query/offset-singlepage', + pagination_query_offset_variable: '/pagination/query/offset-variable/2', +} as const diff --git a/e2e/react/src/utils/testsHelper.ts b/e2e/react/src/utils/testsHelper.ts index 0d8adadb7f..976cf5840f 100644 --- a/e2e/react/src/utils/testsHelper.ts +++ b/e2e/react/src/utils/testsHelper.ts @@ -53,16 +53,9 @@ export async function expect_n_gql( let nbResponse = 0 const listStr: string[] = [] - // function fnReq(request: any) { - // // console.log('>>', request.method(), request.url()); - // if (request.url().endsWith(routes.GraphQL)) { - // nbRequest++; - // } - // } - async function fnRes(response: Response) { // console.log('<<', response.status(), response.url()); - if (response.url().endsWith('/_api')) { + if (response.url().endsWith('/graphql')) { timing.push(new Date().valueOf() - start) try { const json = await response.json() @@ -171,12 +164,8 @@ export async function locator_click(page: Page, selector: string) { * By default goto expect NO graphql response, if you expect some, use: `goto_expect_n_gql` * @returns The response of the page */ -export async function goto( - page: Page, - url: string, - waitUntil: 'domcontentloaded' | 'load' | 'networkidle' | 'commit' = 'domcontentloaded' -): Promise { - const res = await page.goto(url, { waitUntil }) +export async function goto(page: Page, url: string): Promise { + const res = await page.goto(url, { waitUntil: 'domcontentloaded' }) await expect_n_gql(page, null, 0) return res } diff --git a/e2e/react/vite.config.ts b/e2e/react/vite.config.ts index 1102503b2e..9c34cc9856 100644 --- a/e2e/react/vite.config.ts +++ b/e2e/react/vite.config.ts @@ -5,5 +5,8 @@ import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ + server: { + port: process.env.PORT ? parseInt(process.env.PORT) : 5173, + }, plugins: [houdini({ adapter }), react({ fastRefresh: false })], }) diff --git a/packages/create-houdini/templates/react-typescript/package.json b/packages/create-houdini/templates/react-typescript/package.json index 3cce7786e4..e34f760d63 100644 --- a/packages/create-houdini/templates/react-typescript/package.json +++ b/packages/create-houdini/templates/react-typescript/package.json @@ -12,8 +12,8 @@ "houdini": "^HOUDINI_VERSION", "houdini-react": "^HOUDINI_VERSION", "houdini-adapter-auto": "^HOUDINI_VERSION", - "react": "^18.3.0-canary-09fbee89d-20231013", - "react-dom": "^18.3.0-canary-09fbee89d-20231013", + "react": "19.0.0-canary-2b036d3f1-20240327", + "react-dom": "19.0.0-canary-2b036d3f1-20240327", "graphql-yoga": "4.0.4", "graphql": "15.8.0", "@whatwg-node/server": "^0.9.14" @@ -27,7 +27,7 @@ }, "resolutions": { "graphql": "15.8.0", - "react": "18.3.0-canary-09fbee89d-20231013", - "react-dom": "18.3.0-canary-09fbee89d-20231013" + "react": "19.0.0-canary-2b036d3f1-20240327", + "react-dom": "19.0.0-canary-2b036d3f1-20240327" } } diff --git a/packages/create-houdini/templates/react/package.json b/packages/create-houdini/templates/react/package.json index 4f4ea50f58..9ca1789e72 100644 --- a/packages/create-houdini/templates/react/package.json +++ b/packages/create-houdini/templates/react/package.json @@ -12,8 +12,8 @@ "houdini": "^HOUDINI_VERSION", "houdini-react": "^HOUDINI_VERSION", "houdini-adapter-auto": "^HOUDINI_VERSION", - "react": "^18.3.0-canary-09fbee89d-20231013", - "react-dom": "^18.3.0-canary-09fbee89d-20231013", + "react": "19.0.0-canary-2b036d3f1-20240327", + "react-dom": "19.0.0-canary-2b036d3f1-20240327", "graphql-yoga": "4.0.4", "graphql": "15.8.0", "@whatwg-node/server": "^0.9.14" @@ -24,7 +24,7 @@ }, "resolutions": { "graphql": "15.8.0", - "react": "18.3.0-canary-09fbee89d-20231013", - "react-dom": "18.3.0-canary-09fbee89d-20231013" + "react": "19.0.0-canary-2b036d3f1-20240327", + "react-dom": "19.0.0-canary-2b036d3f1-20240327" } } diff --git a/packages/houdini-react/package.json b/packages/houdini-react/package.json index c5b8973cda..764c248c5a 100644 --- a/packages/houdini-react/package.json +++ b/packages/houdini-react/package.json @@ -42,8 +42,8 @@ "graphql": "^15.8.0", "graphql-yoga": "^4.0.4", "houdini": "workspace:^", - "react": "18.3.0-canary-09fbee89d-20231013", - "react-dom": "18.3.0-canary-09fbee89d-20231013", + "react": "19.0.0-canary-2b036d3f1-20240327", + "react-dom": "19.0.0-canary-2b036d3f1-20240327", "react-streaming-compat": "^0.3.18", "recast": "^0.23.1", "rollup": "^3.7.4", diff --git a/packages/houdini-react/src/plugin/vite.tsx b/packages/houdini-react/src/plugin/vite.tsx index 99d63c8b89..4d8ee1bc2a 100644 --- a/packages/houdini-react/src/plugin/vite.tsx +++ b/packages/houdini-react/src/plugin/vite.tsx @@ -261,6 +261,12 @@ export default { initialVariables: variables, }) + // initialize the observer we just created + observer.send({ + setup: true, + variables, + }) + // save it in the cache initialData[artifactName] = observer initialVariables[artifactName] = variables diff --git a/packages/houdini-react/src/runtime/clientPlugin.ts b/packages/houdini-react/src/runtime/clientPlugin.ts index 22a26381af..733557c013 100644 --- a/packages/houdini-react/src/runtime/clientPlugin.ts +++ b/packages/houdini-react/src/runtime/clientPlugin.ts @@ -6,7 +6,7 @@ const plugin: () => ClientPlugin = () => () => { next({ ...ctx, cacheParams: { - ...ctx.fetchParams, + ...ctx.cacheParams, serverSideFallback: false, }, }) diff --git a/packages/houdini-react/src/runtime/hooks/useDocumentHandle.ts b/packages/houdini-react/src/runtime/hooks/useDocumentHandle.ts index 787d9be926..8580449a1a 100644 --- a/packages/houdini-react/src/runtime/hooks/useDocumentHandle.ts +++ b/packages/houdini-react/src/runtime/hooks/useDocumentHandle.ts @@ -15,7 +15,7 @@ import type { } from '$houdini/runtime/lib/types' import React from 'react' -import { useSession } from '../routing/Router' +import { useClient, useSession } from '../routing/Router' export function useDocumentHandle< _Artifact extends QueryArtifact, @@ -36,6 +36,17 @@ export function useDocumentHandle< // grab the current session value const [session] = useSession() + // we want to use a separate observer for pagination queries + const client = useClient() + const paginationObserver = React.useMemo(() => { + // if the artifact doesn't support pagination, don't do anything + if (!artifact.refetch?.paginated) { + return null + } + + return client.observe<_Data, _Input>({ artifact }) + }, [artifact.name]) + // @ts-expect-error: avoiding an as DocumentHandle<_Artifact, _Data, _Input> return React.useMemo>(() => { const wrapLoad = <_Result>( @@ -60,7 +71,9 @@ export function useDocumentHandle< // only consider paginated queries if (artifact.kind !== ArtifactKind.Query || !artifact.refetch?.paginated) { return { + artifact, data: storeValue.data, + variables: storeValue.variables, fetch: fetchQuery, partial: storeValue.partial, } @@ -74,12 +87,12 @@ export function useDocumentHandle< getVariables: () => storeValue.variables!, fetch: fetchQuery, fetchUpdate: (args, updates) => { - return observer.send({ + return paginationObserver!.send({ ...args, cacheParams: { + ...args?.cacheParams, disableSubscriptions: true, applyUpdates: updates, - ...args?.cacheParams, }, session, }) @@ -88,7 +101,9 @@ export function useDocumentHandle< }) return { + artifact, data: storeValue.data, + variables: storeValue.variables, fetch: handlers.fetch, partial: storeValue.partial, loadNext: wrapLoad(setForwardPending, handlers.loadNextPage), @@ -107,7 +122,7 @@ export function useDocumentHandle< storeName: artifact.name, fetch: fetchQuery, fetchUpdate: async (args, updates = ['append']) => { - return observer.send({ + return paginationObserver!.send({ ...args, cacheParams: { disableSubscriptions: true, @@ -120,7 +135,9 @@ export function useDocumentHandle< }) return { + artifact, data: storeValue.data, + variables: storeValue.variables, fetch: handlers.fetch, partial: storeValue.partial, loadNext: wrapLoad(setForwardPending, handlers.loadNextPage), @@ -130,12 +147,14 @@ export function useDocumentHandle< // we don't want to add anything return { + artifact, data: storeValue.data, + variables: storeValue.variables, fetch: fetchQuery, refetch: fetchQuery, partial: storeValue.partial, } - }, [artifact, observer, session, storeValue, true, true]) + }, [artifact, observer, session, storeValue]) } export type DocumentHandle< @@ -145,6 +164,8 @@ export type DocumentHandle< > = { data: _Data partial: boolean + fetch: FetchFn<_Data, Partial<_Input>> + variables: _Input } & RefetchHandlers<_Artifact, _Data, _Input> type RefetchHandlers<_Artifact extends QueryArtifact, _Data extends GraphQLObject, _Input> = diff --git a/packages/houdini-react/src/runtime/hooks/useQueryHandle.ts b/packages/houdini-react/src/runtime/hooks/useQueryHandle.ts index 66a2c66579..ad7af00451 100644 --- a/packages/houdini-react/src/runtime/hooks/useQueryHandle.ts +++ b/packages/houdini-react/src/runtime/hooks/useQueryHandle.ts @@ -133,6 +133,7 @@ export function useQueryHandle< }, }) .then((value) => { + // @ts-expect-error // the final value suspenseUnit.resolved = { ...handle, @@ -155,7 +156,7 @@ export function useQueryHandle< // make sure we prefer the latest store value instead of the initial version we loaded on mount if (!result && suspenseValue?.resolved) { - return suspenseValue.resolved as DocumentHandle<_Artifact, _Data, _Input> + return suspenseValue.resolved as unknown as DocumentHandle<_Artifact, _Data, _Input> } return { diff --git a/packages/houdini-react/src/runtime/routing/Router.tsx b/packages/houdini-react/src/runtime/routing/Router.tsx index b2a4ae08a6..6affa1a213 100644 --- a/packages/houdini-react/src/runtime/routing/Router.tsx +++ b/packages/houdini-react/src/runtime/routing/Router.tsx @@ -11,6 +11,7 @@ import type { RouterManifest, RouterPageManifest } from '$houdini/runtime/router import React from 'react' import { useContext } from 'react' +import { DocumentHandle, useDocumentHandle } from '../hooks/useDocumentHandle' import { useDocumentStore } from '../hooks/useDocumentStore' import { SuspenseCache, suspense_cache } from './cache' @@ -50,7 +51,7 @@ export function Router({ }) // find the matching page for the current route - const [page, variables] = find_match(manifest, currentURL) + const [page, variables] = find_match(configFile, manifest, currentURL) // if we dont have a page, its a 404 if (!page) { throw new Error('404') @@ -108,7 +109,7 @@ export function Router({ // there are 2 things that we could preload: the page component and the data // look for the matching route information - const [page, variables] = find_match(manifest, url) + const [page, variables] = find_match(configFile, manifest, url) // load the page component if necessary if (['both', 'component'].includes(which)) { @@ -199,7 +200,6 @@ function usePageData({ observer .send({ variables: variables, - cacheParams: { disableSubscriptions: true }, session, }) .then(async () => { @@ -246,9 +246,10 @@ function usePageData({ config: configFile, }) )} + }).then(() => { + window.__houdini__nav_caches__?.data_cache.set(artifactName, new_store) }) - window.__houdini__nav_caches__?.data_cache.set(artifactName, new_store) } @@ -589,23 +590,35 @@ const LocationContext = React.createContext<{ pathname: string; params: Record( name: string -): [_Data | null, DocumentStore<_Data, _Input>] { - const store_ref = useRouterContext().data_cache.get(name)! as unknown as DocumentStore< - _Data, - _Input - > +): [_Data | null, DocumentHandle] { + // pull the global context values + const { data_cache, artifact_cache } = useRouterContext() + + // load the store reference (this will suspend) + const store_ref = data_cache.get(name)! as unknown as DocumentStore<_Data, _Input> + // get the live data from the store - const [{ data, errors }, observer] = useDocumentStore<_Data, _Input>({ + const [storeValue, observer] = useDocumentStore<_Data, _Input>({ artifact: store_ref.artifact, observer: store_ref, }) + // pull out the store values we care about + const { data, errors } = storeValue + // if there is an error in the response we need to throw to the nearest boundary if (errors && errors.length > 0) { throw new Error(JSON.stringify(errors)) } + // create the handle that we will use to interact with the store + const handle = useDocumentHandle({ + artifact: artifact_cache.get(name)!, + observer, + storeValue, + }) - return [data, observer] + // we're done + return [data, handle] } function useLinkBehavior({ diff --git a/packages/houdini/src/lib/router/manifest.test.ts b/packages/houdini/src/lib/router/manifest.test.ts index 2a2b35be8d..19aa135b6a 100644 --- a/packages/houdini/src/lib/router/manifest.test.ts +++ b/packages/houdini/src/lib/router/manifest.test.ts @@ -614,6 +614,27 @@ describe('extractQueries', async () => { `, expected: ['title', 'content'], }, + { + name: '$handle', + source: ` + import React from 'react'; + + interface Props { + title: string; + content: string; + } + + const MyComponent = ({ title$handle }: Props) => ( +
+

{title}

+

{content}

+
+ ); + + export default MyComponent; + `, + expected: ['title'], + }, { name: 'Functional component with function expression', source: ` @@ -658,6 +679,20 @@ describe('extractQueries', async () => { `, expected: ['firstName', 'lastName'], }, + { + name: "handle and query don't duplicate", + source: ` + import React from 'react'; + + export default function({ Query, Query$handle }) { + return ( +
+
+ ); + }; + `, + expected: ['Query'], + }, ] for (const testCase of testCases) { diff --git a/packages/houdini/src/lib/router/manifest.ts b/packages/houdini/src/lib/router/manifest.ts index ffec4f2c59..4c9bcd7a76 100644 --- a/packages/houdini/src/lib/router/manifest.ts +++ b/packages/houdini/src/lib/router/manifest.ts @@ -225,6 +225,7 @@ async function add_view(args: { }) { const target = args.type === 'page' ? args.project.pages : args.project.layouts const queries = await extractQueries(args.contents) + // look for any queries that we are asking for that aren't available const missing_queries = queries.filter((query) => !args.queries.includes(query)) if (missing_queries.length > 0) { @@ -387,5 +388,22 @@ export async function extractQueries(source: string): Promise { return [] } - return props.filter((p) => p !== 'children') + return props.reduce((queries, query) => { + // skip the children prop + if (query === 'children') { + return queries + } + + // if the query ends with $handle just use the query name + if (query.endsWith('$handle')) { + query = query.substring(0, query.length - '$handle'.length) + } + + // if the query already exists, don't add it again + if (queries.includes(query)) { + return queries + } + + return queries.concat([query]) + }, []) } diff --git a/packages/houdini/src/runtime/router/match.test.ts b/packages/houdini/src/runtime/router/match.test.ts index f0022225ec..b7a54592fa 100644 --- a/packages/houdini/src/runtime/router/match.test.ts +++ b/packages/houdini/src/runtime/router/match.test.ts @@ -1,5 +1,6 @@ import { test, expect, describe } from 'vitest' +import { testConfigFile } from '../../test' import { exec, find_match, parse_page_pattern } from './match' import type { RouterManifest } from './types' @@ -137,8 +138,10 @@ describe('find_match parse and match', async function () { ), } + const configFile = testConfigFile() + // find the match - const [match] = find_match(manifest, expected) + const [match] = find_match(configFile, manifest, expected) expect(match?.id).toEqual(expected) }) } diff --git a/packages/houdini/src/runtime/router/match.ts b/packages/houdini/src/runtime/router/match.ts index cb48272f2f..d084917c97 100644 --- a/packages/houdini/src/runtime/router/match.ts +++ b/packages/houdini/src/runtime/router/match.ts @@ -1,5 +1,6 @@ import type { GraphQLVariables } from '$houdini/runtime/lib/types' +import { parseScalar, type ConfigFile } from '../lib' import type { RouterManifest, RouterPageManifest } from './types' /** @@ -21,16 +22,19 @@ export interface ParamMatcher { // find the matching page given the current path export function find_match<_ComponentType>( + config: ConfigFile, manifest: RouterManifest<_ComponentType>, current: string, allowNull: true ): [RouterPageManifest<_ComponentType> | null, GraphQLVariables] export function find_match<_ComponentType>( + config: ConfigFile, manifest: RouterManifest<_ComponentType>, current: string, allowNull?: false ): [RouterPageManifest<_ComponentType>, GraphQLVariables] export function find_match<_ComponentType>( + config: ConfigFile, manifest: RouterManifest<_ComponentType>, current: string, allowNull: boolean = true @@ -38,6 +42,7 @@ export function find_match<_ComponentType>( // find the matching path (if it exists) let match: RouterPageManifest<_ComponentType> | null = null let matchVariables: GraphQLVariables = null + for (const page of Object.values(manifest.pages)) { // check if the current url matches const urlMatch = current.match(page.pattern) @@ -55,8 +60,21 @@ export function find_match<_ComponentType>( throw new Error('404') } + // we might have to marshal the variables + let variables: GraphQLVariables = {} + // each of the matched documents might tell us how to handle a subset of the + // matchVariables. look at every document's input specification and marshal + // any values that are in matchVariables + for (const document of Object.values(match?.documents ?? {})) { + for (const [variable, { type }] of Object.entries(document.variables)) { + if (matchVariables?.[variable]) { + variables[variable] = parseScalar(config, type, matchVariables[variable]) + } + } + } + // @ts-ignore - return [match, matchVariables] + return [match, variables] } /** diff --git a/packages/houdini/src/runtime/router/server.ts b/packages/houdini/src/runtime/router/server.ts index 69f4f0d37c..d96930df70 100644 --- a/packages/houdini/src/runtime/router/server.ts +++ b/packages/houdini/src/runtime/router/server.ts @@ -87,7 +87,7 @@ export function _serverHandler({ // the request is for a server-side rendered page // find the matching url - const [match] = find_match(manifest, url) + const [match] = find_match(config_file, manifest, url) // call the framework-specific render hook with the latest session const rendered = await on_render({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f12615edfa..c19b5676c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -566,14 +566,14 @@ importers: specifier: workspace:^ version: link:../houdini react: - specifier: 18.3.0-canary-09fbee89d-20231013 - version: 18.3.0-canary-09fbee89d-20231013 + specifier: 19.0.0-canary-2b036d3f1-20240327 + version: 19.0.0-canary-2b036d3f1-20240327 react-dom: - specifier: 18.3.0-canary-09fbee89d-20231013 - version: 18.3.0-canary-09fbee89d-20231013(react@18.3.0-canary-09fbee89d-20231013) + specifier: 19.0.0-canary-2b036d3f1-20240327 + version: 19.0.0-canary-2b036d3f1-20240327(react@19.0.0-canary-2b036d3f1-20240327) react-streaming-compat: specifier: ^0.3.18 - version: 0.3.18(react-dom@18.3.0-canary-09fbee89d-20231013)(react@18.3.0-canary-09fbee89d-20231013) + version: 0.3.18(react-dom@19.0.0-canary-2b036d3f1-20240327)(react@19.0.0-canary-2b036d3f1-20240327) recast: specifier: ^0.23.1 version: 0.23.1 @@ -582,7 +582,7 @@ importers: version: 3.14.0 use-deep-compare-effect: specifier: ^1.8.1 - version: 1.8.1(react@18.3.0-canary-09fbee89d-20231013) + version: 1.8.1(react@19.0.0-canary-2b036d3f1-20240327) devDependencies: '@types/cookie-parser': specifier: ^1.4.3 @@ -607,7 +607,7 @@ importers: version: 18.0.11 next: specifier: ^13.0.1 - version: 13.1.1(@babel/core@7.20.7)(react-dom@18.3.0-canary-09fbee89d-20231013)(react@18.3.0-canary-09fbee89d-20231013) + version: 13.1.1(@babel/core@7.20.7)(react-dom@19.0.0-canary-2b036d3f1-20240327)(react@19.0.0-canary-2b036d3f1-20240327) scripts: specifier: workspace:^ version: link:../_scripts @@ -8750,7 +8750,7 @@ packages: engines: {node: '>= 0.6'} dev: false - /next@13.1.1(@babel/core@7.20.7)(react-dom@18.3.0-canary-09fbee89d-20231013)(react@18.3.0-canary-09fbee89d-20231013): + /next@13.1.1(@babel/core@7.20.7)(react-dom@19.0.0-canary-2b036d3f1-20240327)(react@19.0.0-canary-2b036d3f1-20240327): resolution: {integrity: sha512-R5eBAaIa3X7LJeYvv1bMdGnAVF4fVToEjim7MkflceFPuANY3YyvFxXee/A+acrSYwYPvOvf7f6v/BM/48ea5w==} engines: {node: '>=14.6.0'} hasBin: true @@ -8772,9 +8772,9 @@ packages: '@swc/helpers': 0.4.14 caniuse-lite: 1.0.30001441 postcss: 8.4.14 - react: 18.3.0-canary-09fbee89d-20231013 - react-dom: 18.3.0-canary-09fbee89d-20231013(react@18.3.0-canary-09fbee89d-20231013) - styled-jsx: 5.1.1(@babel/core@7.20.7)(react@18.3.0-canary-09fbee89d-20231013) + react: 19.0.0-canary-2b036d3f1-20240327 + react-dom: 19.0.0-canary-2b036d3f1-20240327(react@19.0.0-canary-2b036d3f1-20240327) + styled-jsx: 5.1.1(@babel/core@7.20.7)(react@19.0.0-canary-2b036d3f1-20240327) optionalDependencies: '@next/swc-android-arm-eabi': 13.1.1 '@next/swc-android-arm64': 13.1.1 @@ -9483,7 +9483,7 @@ packages: /puppeteer@1.20.0: resolution: {integrity: sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ==} engines: {node: '>=6.4.0'} - deprecated: < 21.5.0 is no longer supported + deprecated: < 21.8.0 is no longer supported requiresBuild: true dependencies: debug: 4.3.4(supports-color@9.3.1) @@ -9570,15 +9570,6 @@ packages: scheduler: 0.19.1 dev: true - /react-dom@18.3.0-canary-09fbee89d-20231013(react@18.3.0-canary-09fbee89d-20231013): - resolution: {integrity: sha512-whQiynFSqI6eQ2KuiQm0lxM10XmMfHzEGN+Z4qGzyQ+hUz8d53rPN2EfmHjQc5GUvqL7z6et7W5d3BeWX69vog==} - peerDependencies: - react: 18.3.0-canary-09fbee89d-20231013 - dependencies: - loose-envify: 1.4.0 - react: 18.3.0-canary-09fbee89d-20231013 - scheduler: 0.24.0-canary-09fbee89d-20231013 - /react-dom@18.3.0-canary-d6dcad6a8-20230914(react@18.3.0-canary-d7a98a5e9-20230517): resolution: {integrity: sha512-KzS+Jy/WXC6I9bi9PtBU0+iMPHPNvNLdyIDJqgX91AiBP9IDDMjaDbgW0QKphi1qIOesYMeJz0uZkajhlfS8lg==} peerDependencies: @@ -9589,6 +9580,14 @@ packages: scheduler: 0.24.0-canary-d6dcad6a8-20230914 dev: false + /react-dom@19.0.0-canary-2b036d3f1-20240327(react@19.0.0-canary-2b036d3f1-20240327): + resolution: {integrity: sha512-pxGk4bDSRFDqVa+hAdkTva+EdMbrxC0X1mR1QC1comx2U2EQfocy1SySSa//m3ivU674sYW7saKDo/fQV8rprw==} + peerDependencies: + react: 19.0.0-canary-2b036d3f1-20240327 + dependencies: + react: 19.0.0-canary-2b036d3f1-20240327 + scheduler: 0.25.0-canary-2b036d3f1-20240327 + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: true @@ -9606,7 +9605,7 @@ packages: engines: {node: '>=0.10.0'} dev: true - /react-streaming-compat@0.3.18(react-dom@18.3.0-canary-09fbee89d-20231013)(react@18.3.0-canary-09fbee89d-20231013): + /react-streaming-compat@0.3.18(react-dom@18.3.0-canary-d6dcad6a8-20230914)(react@18.3.0-canary-d7a98a5e9-20230517): resolution: {integrity: sha512-KyvJHZ3JLQyNQSU/Rg+FYPaU/LGjSrdByE1zHS5DP/I6hxEmDUJYXf9eWdZZMi3lq+sUEwy1P7ije4blb0wF/A==} peerDependencies: react: '>=18' @@ -9615,11 +9614,11 @@ packages: '@brillout/import': 0.2.3 '@brillout/json-serializer': 0.5.6 isbot-fast: 1.2.0 - react: 18.3.0-canary-09fbee89d-20231013 - react-dom: 18.3.0-canary-09fbee89d-20231013(react@18.3.0-canary-09fbee89d-20231013) + react: 18.3.0-canary-d7a98a5e9-20230517 + react-dom: 18.3.0-canary-d6dcad6a8-20230914(react@18.3.0-canary-d7a98a5e9-20230517) dev: false - /react-streaming-compat@0.3.18(react-dom@18.3.0-canary-d6dcad6a8-20230914)(react@18.3.0-canary-d7a98a5e9-20230517): + /react-streaming-compat@0.3.18(react-dom@19.0.0-canary-2b036d3f1-20240327)(react@19.0.0-canary-2b036d3f1-20240327): resolution: {integrity: sha512-KyvJHZ3JLQyNQSU/Rg+FYPaU/LGjSrdByE1zHS5DP/I6hxEmDUJYXf9eWdZZMi3lq+sUEwy1P7ije4blb0wF/A==} peerDependencies: react: '>=18' @@ -9628,8 +9627,8 @@ packages: '@brillout/import': 0.2.3 '@brillout/json-serializer': 0.5.6 isbot-fast: 1.2.0 - react: 18.3.0-canary-d7a98a5e9-20230517 - react-dom: 18.3.0-canary-d6dcad6a8-20230914(react@18.3.0-canary-d7a98a5e9-20230517) + react: 19.0.0-canary-2b036d3f1-20240327 + react-dom: 19.0.0-canary-2b036d3f1-20240327(react@19.0.0-canary-2b036d3f1-20240327) dev: false /react@16.14.0: @@ -9641,12 +9640,6 @@ packages: prop-types: 15.8.1 dev: true - /react@18.3.0-canary-09fbee89d-20231013: - resolution: {integrity: sha512-MV2EqcBD9pAt8IfI5XkPj+/mQwIDPG3CSqbzir8kzjUeETuoMAOTbUl34CH4sS02JBq79jhvrY9L5rmL83OPWg==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - /react@18.3.0-canary-d7a98a5e9-20230517: resolution: {integrity: sha512-WCoMOYGg0OR7IoQ9YhubaJ4j7743LBTx4OOcaRuI4wZkshvPIOuVWrZNOarMuKRj8bm/5DKuAV/p2kd74AbQmg==} engines: {node: '>=0.10.0'} @@ -9654,6 +9647,10 @@ packages: loose-envify: 1.4.0 dev: false + /react@19.0.0-canary-2b036d3f1-20240327: + resolution: {integrity: sha512-dI3DePzDBPIypHcn+84a1H/9IUX67XyK1kCi1KETaKIJrf3LciB1gKSQ5P0G7HEVEIeSKuvpq0QB0uLC3Ta+wA==} + engines: {node: '>=0.10.0'} + /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: @@ -10048,17 +10045,15 @@ packages: object-assign: 4.1.1 dev: true - /scheduler@0.24.0-canary-09fbee89d-20231013: - resolution: {integrity: sha512-EJY9WbhIa1tGdFCDtDmWU/FwwcPrmglpFkQp9IHu5tDjFpbNHMRxO+dles31SPOCIPR6wkRisoLA2/GuNs2niA==} - dependencies: - loose-envify: 1.4.0 - /scheduler@0.24.0-canary-d6dcad6a8-20230914: resolution: {integrity: sha512-tC/9jHWGULTtIk39bb16jrDYyqwz0BHlQlNa3kZYyyFx8JsxioqzT/WoaInIrbkwRaY/zjYzm8IUzE3zH2wKqg==} dependencies: loose-envify: 1.4.0 dev: false + /scheduler@0.25.0-canary-2b036d3f1-20240327: + resolution: {integrity: sha512-ZaJBj3+g9DPMfnsCrCvxQ4G+/6RcH3dRE1dSfB0/mGJB72ZE1agIvwNOGOgbf5wQw4Ka3w3vv0KcHt3jOAH6Lg==} + /selfsigned@2.1.1: resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} engines: {node: '>=10'} @@ -10514,7 +10509,7 @@ packages: acorn: 8.10.0 dev: true - /styled-jsx@5.1.1(@babel/core@7.20.7)(react@18.3.0-canary-09fbee89d-20231013): + /styled-jsx@5.1.1(@babel/core@7.20.7)(react@19.0.0-canary-2b036d3f1-20240327): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -10529,7 +10524,7 @@ packages: dependencies: '@babel/core': 7.20.7 client-only: 0.0.1 - react: 18.3.0-canary-09fbee89d-20231013 + react: 19.0.0-canary-2b036d3f1-20240327 dev: true /stylis@4.1.3: @@ -11318,7 +11313,7 @@ packages: resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} dev: false - /use-deep-compare-effect@1.8.1(react@18.3.0-canary-09fbee89d-20231013): + /use-deep-compare-effect@1.8.1(react@19.0.0-canary-2b036d3f1-20240327): resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==} engines: {node: '>=10', npm: '>=6'} peerDependencies: @@ -11326,7 +11321,7 @@ packages: dependencies: '@babel/runtime': 7.20.7 dequal: 2.0.3 - react: 18.3.0-canary-09fbee89d-20231013 + react: 19.0.0-canary-2b036d3f1-20240327 dev: false /util-deprecate@1.0.2: