A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Scope post counter to new-publish, memoize preview, test the gate

+83 -11
+66
src/components/PublishPanel.test.tsx
··· 137 137 container.remove(); 138 138 } 139 139 140 + /** 141 + * Render a PublishPanel and hand back its container for inspection (no interaction). 142 + * Mirrors the prop shapes the click harnesses above build: a new-article panel passes 143 + * `publications: [ PUB ]` and no `editing`; an editing panel passes `editing: EDITING`. 144 + */ 145 + async function renderPanel( props: { 146 + editing?: typeof EDITING; 147 + publications: unknown[]; 148 + description: string; 149 + } ): Promise< { container: HTMLDivElement; cleanup: () => void } > { 150 + const container = document.createElement( 'div' ); 151 + document.body.appendChild( container ); 152 + const root = createRoot( container ); 153 + await act( async () => { 154 + root.render( 155 + createElement( PublishPanel, { 156 + agent: {} as Agent, 157 + identity: { did: 'did:plc:me', handle: 'me.test' }, 158 + blocks: [ { name: 'core/paragraph', attributes: { content: 'Body' }, innerBlocks: [] } ] as never, 159 + blobRegistry: new Map(), 160 + publications: props.publications as never, 161 + editing: props.editing, 162 + title: 'A title', 163 + description: props.description, 164 + } ) 165 + ); 166 + } ); 167 + const cleanup = () => { 168 + act( () => { 169 + root.unmount(); 170 + } ); 171 + container.remove(); 172 + }; 173 + return { container, cleanup }; 174 + } 175 + 176 + describe( 'PublishPanel post-length gate', () => { 177 + it( 'disables Publish and shows the over-limit counter on a too-long new article', async () => { 178 + const { container, cleanup } = await renderPanel( { 179 + publications: [ PUB ], 180 + description: 'x'.repeat( 320 ), 181 + } ); 182 + const button = Array.from( container.querySelectorAll( 'button' ) ).find( 183 + ( b ) => b.textContent === 'Publish…' 184 + )!; 185 + expect( button.disabled ).toBe( true ); 186 + expect( container.textContent ).toContain( 'Bluesky post:' ); 187 + expect( container.textContent ).toContain( 'too long to publish' ); 188 + cleanup(); 189 + } ); 190 + 191 + it( 'does not disable Update or show a counter when editing the same long content', async () => { 192 + const { container, cleanup } = await renderPanel( { 193 + editing: EDITING, 194 + publications: [], 195 + description: 'x'.repeat( 320 ), 196 + } ); 197 + const button = Array.from( container.querySelectorAll( 'button' ) ).find( 198 + ( b ) => b.textContent === 'Update' 199 + )!; 200 + expect( button.disabled ).toBe( false ); 201 + expect( container.textContent ).not.toContain( 'Bluesky post:' ); 202 + cleanup(); 203 + } ); 204 + } ); 205 + 140 206 describe( 'computePostPreview', () => { 141 207 it( 'reports the mentioned handles and a grapheme count', () => { 142 208 const preview = computePostPreview( {
+17 -11
src/components/PublishPanel.tsx
··· 1 - import { useEffect, useState } from 'react'; 1 + import { useEffect, useMemo, useState } from 'react'; 2 2 import type { Agent } from '@atproto/api'; 3 3 import type { BlockInstance } from '@wordpress/blocks'; 4 4 import { ··· 124 124 const hasTarget = Boolean( target ); 125 125 126 126 const previewHandle = identity.handle ?? identity.did; 127 - const preview = target 128 - ? computePostPreview( { 129 - title, 130 - lede: description, 131 - blocks: normalizeBlocks( blocks ), 132 - handle: previewHandle, 133 - slug: target.slug, 134 - } ) 135 - : null; 127 + const preview = useMemo( 128 + () => 129 + ! isEditing && target 130 + ? computePostPreview( { 131 + title, 132 + lede: description, 133 + blocks: normalizeBlocks( blocks ), 134 + handle: previewHandle, 135 + slug: target.slug, 136 + } ) 137 + : null, 138 + [ isEditing, target, title, description, blocks, previewHandle ] 139 + ); 136 140 137 141 const canSubmit = 138 142 title.trim().length > 0 && blocks.length > 0 && hasTarget && ! preview?.overLimit; ··· 247 251 aria-live="polite" 248 252 > 249 253 Bluesky post: { preview.graphemes }/300 250 - { preview.overLimit ? ' — shorten the subtitle to publish' : '' } 254 + { preview.overLimit 255 + ? ' — too long to publish; shorten the subtitle or remove a mention' 256 + : '' } 251 257 </p> 252 258 ) } 253 259